├── client ├── src │ ├── components │ │ ├── Table-admin-list.js │ │ ├── Empty.js │ │ ├── Loading-gif.js │ │ ├── Items-list-filter-size.js │ │ ├── Button-internal-link.js │ │ ├── Rating-stars.js │ │ ├── Buttons-color-select.js │ │ ├── Item-list-filter-priceBar.js │ │ ├── Button-link-gender-page.js │ │ ├── Secret.js │ │ ├── Footer.js │ │ ├── Paginator.js │ │ ├── Checkout-mini-summary.js │ │ ├── Items-list-htags-labels.js │ │ ├── Button-size-select.js │ │ ├── Hero-banner.js │ │ ├── Admin-form-delete-item.js │ │ ├── Button-add-to-cart.js │ │ ├── Button-sorter.js │ │ ├── Checkout-modal.js │ │ ├── Checkout-mini-summary-preview.js │ │ ├── Item-list-filter-keywords.js │ │ ├── Checkout-step-three.js │ │ ├── Each-item-in-list.js │ │ ├── Admin-table-items.js │ │ ├── Items-list-sidebar.js │ │ ├── Submenu.js │ │ ├── Items-list-banner.js │ │ ├── Admin-table-orders.js │ │ ├── Checkout-step-one.js │ │ ├── Button-filter-mobile.js │ │ ├── Admin.js │ │ ├── Admin-form-add-item.js │ │ ├── Cart.js │ │ ├── Admin-history-log.js │ │ ├── Breadcrumbs.js │ │ ├── Carousel-homepage.js │ │ ├── Carousel-item.js │ │ ├── Items-list-gender-homepage.js │ │ ├── Admin-modal-update.js │ │ ├── Items-list.js │ │ ├── Checkout.js │ │ ├── Item.js │ │ └── Checkout-step-two.js │ ├── actions │ │ ├── UsersActions.js │ │ ├── CartActions.js │ │ └── DataFetchingActions.js │ ├── reducers │ │ ├── usersReducer.js │ │ ├── categoriesProductsReducer.js │ │ ├── cartReducer.js │ │ └── listFetchReducer.js │ ├── index.js │ ├── selectors │ │ ├── selector_list_statistics.js │ │ └── selector_list_products_filter_sorter.js │ ├── containers │ │ ├── Homepage-container.js │ │ ├── Cart-container.js │ │ ├── Item-container.js │ │ ├── Items-list-container.js │ │ ├── Admin-container.js │ │ ├── Checkout-container.js │ │ └── Navbar-container.js │ ├── style │ │ ├── transition.css │ │ ├── rangeslider.min.css │ │ └── checkbox.min.css │ ├── constants.js │ ├── storeConfig.js │ └── Router.js ├── .gitignore ├── public │ ├── favicon.ico │ ├── up-arrow.png │ ├── down-arrow.png │ ├── images │ │ ├── logo.png │ │ ├── menCat.jpg │ │ ├── menCat2.jpg │ │ ├── womenCat.jpg │ │ ├── banner-men.jpg │ │ ├── gifLoading.gif │ │ ├── womenCat2.jpg │ │ ├── banner-men-pc.jpg │ │ ├── banner-cover-pc.jpg │ │ ├── banner-women-pc.jpg │ │ ├── banner-men-mobile.jpg │ │ ├── Polo-red-blue-stripe1.jpg │ │ ├── banner-cover-mobile.jpg │ │ ├── banner-women-mobile.jpg │ │ └── loader.svg │ ├── manifest.json │ └── index.html └── package.json ├── .gitignore ├── config.js ├── models ├── ModelLog.js ├── ModelOrders.js ├── ModelProducts.js └── ModelAdmin.js ├── package.json ├── README.md ├── index.js ├── controllers └── authentication.js ├── services └── passport.js └── routes └── router.js /client/src/components/Table-admin-list.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | yarn.lock 4 | client/yarn.lock -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/components/Checkout-paypal-btn.js -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/up-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/up-arrow.png -------------------------------------------------------------------------------- /client/public/down-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/down-arrow.png -------------------------------------------------------------------------------- /client/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/logo.png -------------------------------------------------------------------------------- /client/public/images/menCat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/menCat.jpg -------------------------------------------------------------------------------- /client/public/images/menCat2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/menCat2.jpg -------------------------------------------------------------------------------- /client/public/images/womenCat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/womenCat.jpg -------------------------------------------------------------------------------- /client/public/images/banner-men.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/banner-men.jpg -------------------------------------------------------------------------------- /client/public/images/gifLoading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/gifLoading.gif -------------------------------------------------------------------------------- /client/public/images/womenCat2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/womenCat2.jpg -------------------------------------------------------------------------------- /client/public/images/banner-men-pc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/banner-men-pc.jpg -------------------------------------------------------------------------------- /client/public/images/banner-cover-pc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/banner-cover-pc.jpg -------------------------------------------------------------------------------- /client/public/images/banner-women-pc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/banner-women-pc.jpg -------------------------------------------------------------------------------- /client/public/images/banner-men-mobile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/banner-men-mobile.jpg -------------------------------------------------------------------------------- /client/src/components/Empty.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Empty = () =>

Empty

; 4 | 5 | 6 | export default Empty; 7 | -------------------------------------------------------------------------------- /client/public/images/Polo-red-blue-stripe1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/Polo-red-blue-stripe1.jpg -------------------------------------------------------------------------------- /client/public/images/banner-cover-mobile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/banner-cover-mobile.jpg -------------------------------------------------------------------------------- /client/public/images/banner-women-mobile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elibenjii/ecommerce-react/HEAD/client/public/images/banner-women-mobile.jpg -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | secret: 'MY SECRET', 4 | DB_URI_DEV: 'mongodb://mongodb0.example.com:27017/admin', 5 | DB_URI: 'mongodb://mongodb0.example.com:27017/admin' 6 | }; -------------------------------------------------------------------------------- /client/src/actions/UsersActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | USER_ADDRESS 3 | } from '../constants.js'; 4 | 5 | export const addUserAddress = infoUser => ({ 6 | type: USER_ADDRESS, 7 | infoUser 8 | }); 9 | -------------------------------------------------------------------------------- /client/src/components/Loading-gif.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const LoadingGif = () =>
4 | 5 | export default LoadingGif 6 | -------------------------------------------------------------------------------- /client/src/reducers/usersReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | USER_ADDRESS 3 | } from '../constants.js'; 4 | 5 | export const getUserAddress = (state = {}, action) => { 6 | switch (action.type) { 7 | case USER_ADDRESS: 8 | return action.infoUser; 9 | 10 | default: 11 | return state; 12 | } 13 | }; -------------------------------------------------------------------------------- /models/ModelLog.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const LogSchema = mongoose.Schema({ 4 | type: { 5 | type: String, 6 | required: true, 7 | }, 8 | time: Date, 9 | itemid: String, 10 | itemtitle: String 11 | }); 12 | 13 | const ModelLog = mongoose.model('logs', LogSchema); 14 | 15 | module.exports = ModelLog; 16 | 17 | -------------------------------------------------------------------------------- /client/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": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom' 4 | import { Provider } from 'react-redux' 5 | import Router from './Router'; 6 | import { Store } from './storeConfig' 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | 14 | 15 | , document.getElementById('root')); -------------------------------------------------------------------------------- /client/src/actions/CartActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_TO_CART, 3 | DELETE_FROM_CART, 4 | DELETE_ALL_FROM_CART 5 | } from '../constants'; 6 | 7 | export const addToCart = item => ({ 8 | type: ADD_TO_CART, 9 | item 10 | }); 11 | 12 | export const deleteFromCart = item => ({ 13 | type: DELETE_FROM_CART, 14 | item 15 | }); 16 | 17 | export const deleteALlFromCart = item => ({ 18 | type: DELETE_ALL_FROM_CART, 19 | item 20 | }); -------------------------------------------------------------------------------- /client/src/reducers/categoriesProductsReducer.js: -------------------------------------------------------------------------------- 1 | import { CATEGORIES_PRODUCTS } from '../constants.js' 2 | 3 | const defaultState = 4 | { 5 | men: ['Polos', 'Shirts', 'Pants', 'Jackets'], 6 | women: ['Dresses', 'Cardigans', 'Tops', 'Trench Coats'] 7 | } 8 | 9 | export const categoriesProducts = (state = defaultState, action) => { 10 | switch (action.type) { 11 | case CATEGORIES_PRODUCTS: 12 | return state; 13 | 14 | default: 15 | return state; 16 | } 17 | } -------------------------------------------------------------------------------- /client/src/selectors/selector_list_statistics.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | const getCart = state => state.cartReducer; 4 | const totalItemsInCart = (cartReducer) => cartReducer.reduce((acc, x) => acc + x.quantity, 0); 5 | const totalAmountCart = (cartReducer) => cartReducer.reduce((acc, x) => acc + (x.quantity * x.price), 0); 6 | 7 | export const selectorTotalItemsCart = createSelector( 8 | getCart, 9 | totalItemsInCart 10 | ); 11 | 12 | export const selectorTotalAmountCart = createSelector( 13 | getCart, 14 | totalAmountCart 15 | ); -------------------------------------------------------------------------------- /client/src/containers/Homepage-container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | import HeroBanner from '../components/Hero-banner'; 4 | import CarouselHomepage from '../components/Carousel-homepage'; 5 | 6 | 7 | const styles = {marginTop:'-33px'}; 8 | 9 | const Homepage = () => ( 10 |
11 | 12 | Demo Ecommerce template 13 | 14 | 15 | 16 |
17 |
18 | ); 19 | 20 | export default Homepage; -------------------------------------------------------------------------------- /models/ModelOrders.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const OrdersSchema = mongoose.Schema({ 4 | ref: String, 5 | customerinfo: { email: String, firstName: String, lastName: String, country: String, city: String, province: String, postalCode: Number, phoneNumber: Number, address1: String, address2: String }, 6 | order: [{ idItem: String, titleItem: String, selectedSize: String, selectedColor: String, price: Number, quantity: Number }], 7 | totalDelivery: Number, 8 | totalAmount: Number 9 | }, { timestamps: true }) 10 | 11 | const ModelOrders = mongoose.model('orders', OrdersSchema) 12 | 13 | module.exports = ModelOrders -------------------------------------------------------------------------------- /models/ModelProducts.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const ProductsSchema = mongoose.Schema({ 4 | title: { 5 | type: String, 6 | required: true, 7 | }, 8 | size: [{type: String}], 9 | price: { 10 | type: Number, 11 | required: true 12 | }, 13 | images: [{type: String}], 14 | rating: { 15 | type: Number, 16 | min: 0, 17 | max: 5 18 | 19 | }, 20 | tags: [{type: String}], 21 | color: [{type: String}], 22 | description: String 23 | }) 24 | 25 | const ModelProducts = mongoose.model('product', ProductsSchema) 26 | 27 | module.exports = ModelProducts 28 | 29 | -------------------------------------------------------------------------------- /client/src/components/Items-list-filter-size.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { Button } from 'reactstrap'; 4 | 5 | const propTypes = { 6 | dispatchSize: PropTypes.func.isRequired, 7 | sortSizeForFilter: PropTypes.string.isRequired 8 | }; 9 | 10 | const availableSizes = ['All', 'XL', 'L', 'M', 'S', 'XS']; 11 | 12 | const ItemsListFilterSize = ({dispatchSize, sortSizeForFilter}) => availableSizes.map(x=>); 13 | 14 | ItemsListFilterSize.propTypes = propTypes; 15 | 16 | export default ItemsListFilterSize; 17 | -------------------------------------------------------------------------------- /client/src/containers/Cart-container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { addToCart, deleteFromCart, deleteALlFromCart } from '../actions/CartActions'; 4 | import Cart from '../components/Cart'; 5 | 6 | const CartContainer = props => ; 7 | 8 | const mapStateToProps = state => ({ getCart: state.cartReducer }) 9 | 10 | const mapDispatchToProps = dispatch => ({ 11 | addToCart: x => dispatch(addToCart(x)), 12 | deleteFromCart: x => dispatch(deleteFromCart(x)), 13 | deleteALlFromCart: x => dispatch(deleteALlFromCart(x)) 14 | }) 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(CartContainer) 17 | -------------------------------------------------------------------------------- /client/src/components/Button-internal-link.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { Button } from 'reactstrap'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | const propTypes = { 7 | link: PropTypes.string.isRequired, 8 | sizeBtn: PropTypes.string.isRequired, 9 | lightOrDark: PropTypes.string.isRequired, 10 | content: PropTypes.string.isRequired 11 | }; 12 | 13 | const ButtonInternalLink = ({link, sizeBtn, lightOrDark, content}) => 14 | 15 | 18 | ; 19 | 20 | ButtonInternalLink.propTypes = propTypes; 21 | 22 | export default ButtonInternalLink; 23 | -------------------------------------------------------------------------------- /client/src/style/transition.css: -------------------------------------------------------------------------------- 1 | .fadeTranslate-enter { 2 | opacity: 0; 3 | transform: translate(0, -3vh); 4 | position: fixed; 5 | } 6 | 7 | .fadeTranslate-enter.fadeTranslate-enter-active { 8 | opacity: 1; 9 | transform: translate(0, 0); 10 | transition: opacity 500ms ease-in 500ms, transform 500ms ease-in-out 500ms; 11 | } 12 | 13 | .fadeTranslate-exit { 14 | opacity: 1; 15 | position: fixed; 16 | transform: translate(0, 0); 17 | } 18 | 19 | .fadeTranslate-exit.fadeTranslate-exit-active { 20 | opacity: 0; 21 | transform: translate(0, 3vh); 22 | transition: opacity 300ms ease-in, transform 300ms ease-in-out; 23 | } 24 | 25 | .fix-container { 26 | position: fixed; 27 | } -------------------------------------------------------------------------------- /client/src/components/Rating-stars.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import StarRatings from 'react-star-ratings'; 3 | 4 | class RatingStars extends Component { 5 | 6 | constructor(props){ 7 | super(props); 8 | this.state = { 9 | rating: 3 10 | } 11 | }; 12 | 13 | 14 | changeRating = ( newRating, name ) => { 15 | this.setState({ 16 | rating: newRating 17 | }); 18 | } 19 | 20 | render = () => 21 | 30 | }; 31 | 32 | export default RatingStars; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommerce-react", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "node index.js", 8 | "heroku-postbuild": "cd client && yarn && yarn run build", 9 | "client": "cd client && yarn start", 10 | "server": "nodemon index.js", 11 | "dev": "concurrently --kill-others-on-fail \"yarn server\" \"yarn client\"" 12 | }, 13 | "dependencies": { 14 | "bcrypt-nodejs": "0.0.3", 15 | "body-parser": "1.18.3", 16 | "compression": "1.7.3", 17 | "dotenv": "6.1.0", 18 | "express": "4.19.2", 19 | "jwt-simple": "0.5.5", 20 | "mongoose": "5.13.20", 21 | "morgan": "1.9.1", 22 | "nodemailer": "6.9.9", 23 | "passport": "0.4.0", 24 | "passport-jwt": "4.0.0", 25 | "passport-local": "1.0.0" 26 | }, 27 | "devDependencies": { 28 | "concurrently": "4.0.1", 29 | "nodemon": "1.18.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /models/ModelAdmin.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | const bcrypt = require('bcrypt-nodejs'); 4 | 5 | // Define our model 6 | const userSchema = new Schema({ 7 | email: { type: String, unique: true, lowercase: true }, 8 | password: String 9 | }); 10 | 11 | 12 | userSchema.pre('save', function(next){ 13 | const user = this; 14 | 15 | bcrypt.genSalt(10, function(err, salt){ 16 | if(err){return next(err);} 17 | 18 | bcrypt.hash(user.password, salt, null, function(err, hash){ 19 | if(err) return next(err); 20 | user.password = hash; 21 | next(); 22 | }); 23 | }) 24 | }); 25 | 26 | userSchema.methods.comparePassword = function(candidatePassword, callback){ 27 | bcrypt.compare(candidatePassword, this.password, function(err, isMatch){ 28 | if(err){return callback(err);} 29 | callback(null, isMatch); 30 | }); 31 | } 32 | 33 | const ModelClass = mongoose.model('admins', userSchema); 34 | 35 | module.exports = ModelClass; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React/Node.js template (MERN stack) ecommerce (light bundle: 100ko). 2 | 3 | ## Demo 4 | 5 | https://demo-react-eco2.herokuapp.com/ 6 | 7 | https://demo-react-eco2.herokuapp.com/admin (user: admin, psw: admin) 8 | 9 | ## Prerequisites 10 | 11 | Node.js, MongoDB and npm or yarn package manager are required 12 | 13 | ## Installation 14 | You will only need to install the modules and import your DB. 15 | You can run a demo collection from file 'democollections.json' 16 | 17 | Installing the modules & importing the products list DB: 18 | 19 | ``` 20 | $ yarn 21 | $ cd client && yarn start 22 | import democollections.json 23 | setup your config.js file (secret JWT & Mongo URI) 24 | ``` 25 | 26 | ## Features 27 | - cached DB for fast results 28 | - filters combinables bewteen each other 29 | - validators 30 | 31 | ## Websites using this template 32 | - Headshot generator using AI (convert selfies into corporate photos): https://www.headshotgenerator.io/ 33 | -------------------------------------------------------------------------------- /client/src/constants.js: -------------------------------------------------------------------------------- 1 | export const LIST_ERROR = 'LIST_ERROR'; 2 | export const LIST_IS_LOADING = 'LIST_IS_LOADING' 3 | export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS' 4 | export const ITEM_FETCH_SUCCESS = 'ITEM_FETCH_SUCCESS' 5 | export const ITEM_ERROR = 'ITEM_ERROR' 6 | export const ITEM_IS_LOADING = 'ITEM_IS_LOADING' 7 | export const FILL_FILTER = 'FILL_FILTER' 8 | 9 | export const FILTER_ARGS = 'FILTER_ARGS' 10 | export const FILTER_CATEGORIES_MULTIPLE_KEYWORDS = 'FILTER_CATEGORIES_MULTIPLE_KEYWORDS' 11 | export const FILTER_CATEGORIES_ONE_KEYWORD = 'FILTER_CATEGORIES_ONE_KEYWORD' 12 | export const FILTER_SIZE = 'FILTER_SIZE' 13 | export const FILTER_PRICE_RANGE = 'FILTER_PRICE_RANGE' 14 | export const RESET_KEYWORDS = 'RESET_KEYWORDS' 15 | 16 | export const CATEGORIES_PRODUCTS = 'CATEGORIES_PRODUCTS' 17 | 18 | export const ADD_TO_CART = 'ADD_TO_CART' 19 | export const DELETE_FROM_CART = 'DELETE_FROM_CART' 20 | export const DELETE_ALL_FROM_CART = 'DELETE_ALL_FROM_CART' 21 | 22 | export const USER_ADDRESS = 'USER_ADDRESS' -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": ">=0.19.0", 7 | "email-prop-type": "3.0.0", 8 | "prop-types": "15.6.2", 9 | "react": "16.5.2", 10 | "react-chartjs-2": "2.7.4", 11 | "react-device-detect": "1.6.1", 12 | "react-dom": "16.5.2", 13 | "react-helmet": "5.2.0", 14 | "react-icons": "3.2.2", 15 | "react-loadable": "5.5.0", 16 | "react-paypal-express-checkout": "1.0.5", 17 | "react-rangeslider": "2.2.0", 18 | "react-redux": "5.0.7", 19 | "react-router-dom": "4.3.1", 20 | "react-scripts": "1.1.5", 21 | "react-star-ratings": "2.3.0", 22 | "reactstrap": "6.4.0", 23 | "redux": "4.0.0", 24 | "redux-thunk": "2.3.0", 25 | "reselect": "4.0.0" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test --env=jsdom", 31 | "eject": "react-scripts eject" 32 | }, 33 | "proxy": "http://localhost:5000/" 34 | } 35 | -------------------------------------------------------------------------------- /client/src/components/Buttons-color-select.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | const propTypes = { 5 | colors: PropTypes.array.isRequired, 6 | selectedColor: PropTypes.string.isRequired, 7 | handleColorSelection: PropTypes.func.isRequired, 8 | validateColorSelection: PropTypes.func.isRequired, 9 | }; 10 | 11 | const styles = (x, selectedColor) => ({ 12 | backgroundColor: x, 13 | margin:'3px', 14 | width: '30px', 15 | height: '30px', 16 | display: 'inline-block', 17 | cursor: 'pointer', 18 | boxShadow: x === selectedColor ? '0px 0px 6px 1px rgba(0,0,0,1)' : '0px 0px 2px 1px rgba(0,0,0,1)' 19 | }); 20 | 21 | const ButtonsColorSelect = ({ colors, handleColorSelection, selectedColor, validateColorSelection }) => ( 22 | colors.map(x =>
{return (handleColorSelection(x), validateColorSelection('valid'))}} style={styles(x, selectedColor)} />) 23 | ); 24 | 25 | ButtonsColorSelect.propTypes = propTypes; 26 | 27 | export default ButtonsColorSelect; 28 | -------------------------------------------------------------------------------- /client/src/components/Item-list-filter-priceBar.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import Slider from 'react-rangeslider'; 4 | import '../style/rangeslider.min.css'; 5 | 6 | const propTypes = { 7 | actionPriceRangeFilter: PropTypes.func.isRequired, 8 | reducerPriceRangeFilter: PropTypes.number.isRequired, 9 | }; 10 | 11 | const styles = { 12 | display: 'flex', 13 | justifyContent: 'space-around' 14 | }; 15 | 16 | const ProductListFilterPriceBar = ({ 17 | actionPriceRangeFilter, 18 | reducerPriceRangeFilter 19 | }) => ( 20 |
21 | 27 |
28 | 29 | min 30 | 31 | 32 | max 33 | 34 |
35 |
36 | ); 37 | 38 | ProductListFilterPriceBar.propTypes = propTypes; 39 | 40 | export default ProductListFilterPriceBar; -------------------------------------------------------------------------------- /client/src/components/Button-link-gender-page.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react' 3 | import { 4 | isMobile 5 | } from "react-device-detect"; 6 | import { Button } from 'reactstrap'; 7 | import { Link } from 'react-router-dom' 8 | 9 | const propTypes = { 10 | gender: PropTypes.string.isRequired 11 | }; 12 | 13 | const styles = { 14 | centerButtons: { 15 | textAlign: 'center', 16 | padding: '30px' 17 | }, 18 | buttonStylePc: { 19 | margin:'20px', 20 | padding: '15px' 21 | }, 22 | buttonStyleMobile: { 23 | margin:'5px', 24 | padding: '15px' 25 | } 26 | } 27 | 28 | const ButtonLinkGenderPage = ({gender, content=gender}) => { 29 | 30 | const {buttonStylePc, buttonStyleMobile } = styles 31 | 32 | return 33 | } 34 | 35 | ButtonLinkGenderPage.propTypes = propTypes; 36 | 37 | export default ButtonLinkGenderPage -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express') 3 | const morgan = require('morgan') 4 | const bodyParser = require('body-parser') 5 | const mongoose = require('mongoose').set('debug', true); 6 | const path = require('path'); 7 | require('dotenv').config(); 8 | require('./services/passport.js') 9 | const compression = require('compression') 10 | const config = require('./config.js'); 11 | 12 | const env = process.env.NODE_ENV || 'development'; 13 | 14 | mongoose.connect(env === 'development' ? config.DB_URI_DEV : config.DB_URI, {useUnifiedTopology: true, useNewUrlParser: true}) 15 | 16 | const app = express() 17 | app.use(compression()) 18 | 19 | env !== 'development' && app.use(express.static(path.join(__dirname, 'client/build'))); 20 | 21 | 22 | env === 'development' && app.use(morgan('dev')) 23 | app.use(bodyParser.json()) 24 | app.use('/api', require('./routes/router')) 25 | 26 | env !== 'development' && app.get('*', (req, res) => { 27 | res.sendFile(path.join(__dirname+'/client/build/index.html')); 28 | }); 29 | 30 | app.listen(process.env.PORT || 5000) 31 | -------------------------------------------------------------------------------- /client/src/components/Secret.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import axios from 'axios'; 3 | import { Link } from "react-router-dom"; 4 | import Admin from './Admin'; 5 | 6 | 7 | class Secret extends Component { 8 | constructor(props){ 9 | super(props); 10 | this.state = { 11 | authorization: false, 12 | apiAuth: false 13 | }; 14 | } 15 | 16 | async componentDidMount() { 17 | try { 18 | const response = await axios({ 19 | method:'get', 20 | url:'/api/secret', 21 | headers: {'Authorization': localStorage.getItem("token")} 22 | }) 23 | const apiAuth = await response.data.authorization; 24 | this.setState({ apiAuth }) 25 | } catch (error) { 26 | console.log(error); 27 | } 28 | } 29 | 30 | render() { 31 | const showAdminPanel = this.state.apiAuth ? :

Authorization is required, please login here: login

32 | 33 | return ( 34 |
35 | {showAdminPanel} 36 |
37 | ) 38 | } 39 | } 40 | 41 | export default Secret 42 | -------------------------------------------------------------------------------- /controllers/authentication.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/ModelAdmin'); 2 | const config = require('../config'); 3 | const jwt = require('jwt-simple'); 4 | 5 | function tokenForUser(user){ 6 | const timestamp = new Date().getTime(); 7 | return jwt.encode({sub: user.id, iat: timestamp}, config.secret); 8 | } 9 | 10 | exports.signin = function(req, res, next){ 11 | res.send({token: tokenForUser(req.user)}); 12 | } 13 | 14 | exports.signup = function(req, res, next){ 15 | const email = req.body.email; 16 | const password = req.body.password; 17 | 18 | if(!email || !password){ 19 | return res.status(422).json({error: 'You must provide email and password.'}); 20 | } 21 | 22 | User.findOne({email: email}, function(err, existingUser){ 23 | if(err){return next(err);} 24 | 25 | if(existingUser){ 26 | return res.status(422).json({error: 'Email is in use'}); 27 | } 28 | 29 | const user = new User({ 30 | email: email, 31 | password: password 32 | }); 33 | 34 | user.save(function(err){ 35 | if(err){return next(err);} 36 | res.json({token: tokenForUser(user)}); 37 | }); 38 | }); 39 | } -------------------------------------------------------------------------------- /client/src/storeConfig.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk'; 3 | import { 4 | listFetchDataSuccess, 5 | listHasError, 6 | listIsLoading, 7 | sortArgsForFilter, 8 | keywordsForFilter, 9 | sortSizeForFilter, 10 | itemFetchDataSuccess, 11 | itemHasError, 12 | itemIsLoading, 13 | reducerPriceRangeFilter 14 | } from './reducers/listFetchReducer'; 15 | import { categoriesProducts } from './reducers/categoriesProductsReducer' 16 | import { getUserAddress } from './reducers/usersReducer' 17 | import { cartReducer } from './reducers/cartReducer' 18 | 19 | const rootReducer = combineReducers({ 20 | listFetchDataSuccess, 21 | itemFetchDataSuccess, 22 | listHasError, 23 | listIsLoading, 24 | itemHasError, 25 | itemIsLoading, 26 | sortArgsForFilter, 27 | keywordsForFilter, 28 | sortSizeForFilter, 29 | categoriesProducts, 30 | cartReducer, 31 | reducerPriceRangeFilter, 32 | getUserAddress 33 | }) 34 | 35 | export const Store = createStore( 36 | rootReducer, 37 | applyMiddleware(thunk) 38 | ) 39 | -------------------------------------------------------------------------------- /client/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Container, Row, Col } from 'reactstrap'; 3 | 4 | const styles = { 5 | backgroundColor: '#072a48', 6 | paddingTop: '50px', 7 | paddingBottom: '50px', 8 | color: 'white', 9 | textAlign: 'center' 10 | } 11 | 12 | const Footer = () => ( 13 |
14 | 15 | 16 | Your footer 17 | 18 | 19 | About the brand 20 | Career 21 | Instagram 22 | 23 | 24 | Order status 25 | Our ecological actions 26 | Facebook 27 | 28 | 29 | Contact us 30 | I like Som tum 31 | Pinterest 32 | 33 | 34 | Copyright your website © 2018 All Rights Reserved 35 | 36 | 37 |
38 | ) 39 | 40 | export default Footer; -------------------------------------------------------------------------------- /client/src/components/Paginator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | Pagination, 5 | PaginationItem, 6 | PaginationLink 7 | } from 'reactstrap'; 8 | 9 | const propTypes = { 10 | currentPage: PropTypes.number, 11 | maxPages: PropTypes.number, 12 | onPageChange: PropTypes.func 13 | } 14 | 15 | const previousBtn = (currentPage, onPageChange) => currentPage > 1 && onPageChange('previous')}/> 16 | const nextBtn = (currentPage, maxPages, onPageChange) => currentPage < maxPages && onPageChange('next')}/> 17 | 18 | const Paginator = ({currentPage, maxPages, onPageChange}) => ( 19 | 20 | 21 | 22 | {previousBtn(currentPage, onPageChange)} 23 | 24 | {[...Array(maxPages)].map((x, i) =>( 25 | 26 | onPageChange(i)}> 27 | {i+1} 28 | 29 | 30 | ) 31 | )} 32 | 33 | 34 | {nextBtn(currentPage, maxPages, onPageChange)} 35 | 36 | ); 37 | 38 | Paginator.propTypes = propTypes; 39 | 40 | export default Paginator; -------------------------------------------------------------------------------- /client/src/components/Checkout-mini-summary.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react' 3 | import { ListGroup, ListGroupItem, Col, Row } from 'reactstrap'; 4 | 5 | const propTypes = { getCart: PropTypes.array.isRequired }; 6 | 7 | const CheckoutMiniSummary = ({ getCart, selectorTotalAmountCart, totalDelivery }) => ( 8 | 9 | Order Summary 10 | 11 | { 12 | getCart.map(x=> 13 | 14 |

x{x.quantity} {x.title}

15 |

{x.price}$

16 |
17 | ) 18 | } 19 |
20 | 21 | 22 |

Subtotal

23 |

{selectorTotalAmountCart}$

24 |
25 | 26 |

Shipping

27 |

{totalDelivery}$

28 |
29 | 30 |

Tax

31 |

0$

32 |
33 |
34 | 35 | 36 |

Total

37 | {selectorTotalAmountCart+totalDelivery}$ 38 |
39 |
40 |
41 | ); 42 | 43 | CheckoutMiniSummary.propTypes = propTypes; 44 | 45 | export default CheckoutMiniSummary; 46 | -------------------------------------------------------------------------------- /client/src/components/Items-list-htags-labels.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { Badge } from 'reactstrap'; 4 | 5 | const propTypes = { 6 | reducerPriceRangeFilter: PropTypes.number.isRequired, 7 | sortArgsForFilter: PropTypes.string.isRequired, 8 | sortSizeForFilter: PropTypes.string.isRequired, 9 | keywordsForFilter: PropTypes.array.isRequired, 10 | }; 11 | 12 | const ItemsListHtagsLabel = ({ 13 | reducerPriceRangeFilter, 14 | sortSizeForFilter, 15 | keywordsForFilter, 16 | sortArgsForFilter 17 | }) => { 18 | 19 | const priceLabel = keywordsForFilter.length> 0 && {`#Price<${reducerPriceRangeFilter} $`} 20 | const sizeLabel = keywordsForFilter.length> 0 && {`#Size${sortSizeForFilter === 'All' ? 's' : ''}: ${sortSizeForFilter}`} 21 | const sortLabel = keywordsForFilter.length> 0 && {`#Sort: ${sortArgsForFilter}`} 22 | const categoriesLabel = keywordsForFilter.map(x=>{`#${x}`} ) 23 | return ( 24 |
25 | { sortLabel } 26 | { priceLabel } 27 | { sizeLabel } 28 | { categoriesLabel } 29 |
30 | ) 31 | }; 32 | 33 | ItemsListHtagsLabel.propTypes = propTypes; 34 | 35 | export default ItemsListHtagsLabel; 36 | -------------------------------------------------------------------------------- /client/src/components/Button-size-select.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; 4 | 5 | const propTypes = { 6 | handleSizeSelection: PropTypes.func.isRequired, 7 | sizesArray: PropTypes.array.isRequired, 8 | selectedSize: PropTypes.string.isRequired, 9 | validateSizeSelection: PropTypes.func.isRequired 10 | }; 11 | 12 | class ButtonSizeSelect extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | dropdownOpen: false 17 | }; 18 | }; 19 | 20 | toggle = () => { 21 | this.setState(prevState => ({ 22 | dropdownOpen: !prevState.dropdownOpen 23 | })); 24 | }; 25 | 26 | render() { 27 | const { handleSizeSelection, sizesArray, selectedSize, validateSizeSelection} = this.props; 28 | const dropDownList = sizesArray.map(x=> 29 | {return (handleSizeSelection(x), validateSizeSelection('valid'))}}>{x} 30 | ); 31 | return ( 32 | 33 | 34 | Size: {selectedSize.length>0 ? selectedSize : 'Click to choose'} 35 | 36 | 37 | {dropDownList} 38 | 39 | 40 | ); 41 | }; 42 | }; 43 | 44 | ButtonSizeSelect.propTypes = propTypes; 45 | 46 | export default ButtonSizeSelect; -------------------------------------------------------------------------------- /client/src/components/Hero-banner.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | isMobile 4 | } from "react-device-detect"; 5 | import ButtonLinkGenderPage from './Button-link-gender-page' 6 | import { Jumbotron, Container } from 'reactstrap'; 7 | 8 | const styles = { 9 | bannerCoverPc: { 10 | backgroundImage: 'url("/images/banner-cover-pc.jpg")', 11 | backgroundSize: 'cover' 12 | }, 13 | bannerCoverMobile: { 14 | backgroundImage: 'url("/images/banner-cover-mobile.jpg")', 15 | backgroundSize: 'cover' 16 | }, 17 | textBanner: { 18 | textShadow: "3px 3px 3px grey", 19 | textAlign: 'center', 20 | color:'white' 21 | }, 22 | centerButtons: { 23 | textAlign: 'center', 24 | padding: '30px' 25 | }, 26 | titleH1Pc: { 27 | fontSize: '80px' 28 | }, 29 | titleH1Mobile: { 30 | fontSize: '60px' 31 | } 32 | }; 33 | 34 | const { bannerCoverPc, bannerCoverMobile, textBanner, centerButtons, titleH1Mobile, titleH1Pc } = styles 35 | 36 | const HeroBanner = () => ( 37 | 38 | 39 |
40 |

Fashion shop

41 |

Cool shop

42 |
43 |
44 | 45 | 46 |
47 |
48 |
49 | ); 50 | 51 | export default HeroBanner; -------------------------------------------------------------------------------- /services/passport.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const User = require('../models/ModelAdmin'); 3 | const config = require('../config.js'); 4 | const JwtStrategy = require('passport-jwt').Strategy; 5 | const ExtractJwt = require('passport-jwt').ExtractJwt; 6 | const LocalStrategy = require('passport-local'); 7 | 8 | //Create local strategy 9 | const localOptions = { 10 | usernameField: 'email' 11 | }; 12 | 13 | passport.use(new LocalStrategy(localOptions, function(email,password,done){ 14 | //Verify this email and password, call done with the user 15 | //if it is the correct email and password 16 | //otherwise, call done with false 17 | User.findOne({email: email}, function(err, user){ 18 | if(err){return done(err);} 19 | if(!user){return done(null, false);} 20 | 21 | //compare passwords - is password equal to user.password? 22 | user.comparePassword(password, function(err, isMatch){ 23 | if(err){return done(err);} 24 | if(!isMatch){return done(null, false);} 25 | return done(null, user); 26 | }) 27 | 28 | }); 29 | })); 30 | 31 | //Setup options foor JWT Strategy 32 | const jwtOptions = { 33 | jwtFromRequest: ExtractJwt.fromHeader('authorization'), 34 | secretOrKey: config.secret 35 | }; 36 | 37 | //Create JWT Strategy 38 | passport.use(new JwtStrategy(jwtOptions, function(payload, done){ 39 | //See if the user ID in the payload exists in our database 40 | //If it does, call 'done' with that user 41 | //otherwise, call done without a user object 42 | User.findById(payload.sub, function(err, user){ 43 | //second argument is user object if we found one 44 | if(err) {return done(err, false);} 45 | 46 | if(user){ 47 | done(null, user); 48 | }else{ 49 | done(null, false); 50 | } 51 | }); 52 | })); 53 | 54 | -------------------------------------------------------------------------------- /client/src/components/Admin-form-delete-item.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; 3 | import { FiXCircle } from 'react-icons/fi'; 4 | import axios from 'axios'; 5 | 6 | class AdminFormDeleteItem extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | modalEdit: false, 11 | }; 12 | } 13 | 14 | handleDelete = id => { 15 | axios.delete('/api/delete/item/', { 16 | params: { 17 | id 18 | } 19 | }) 20 | .then(response => { 21 | console.log(response); 22 | this.setState({ modalEdit: !this.state.modalEdit }); 23 | }) 24 | .then(() => { 25 | window.location.reload(true) 26 | }) 27 | .catch(error => { 28 | console.log(error); 29 | }); 30 | } 31 | 32 | toggle = () => this.setState({ modalEdit: !this.state.modalEdit }); 33 | 34 | render() { 35 | const { title, id } = this.props 36 | return ( 37 |
38 | 39 | 40 | {title} 41 | 42 | You confirm deleting item ID: {id}? 43 | 44 | 45 | {' '} 48 | 49 | 50 | 51 |
52 | ); 53 | } 54 | } 55 | 56 | export default AdminFormDeleteItem; 57 | -------------------------------------------------------------------------------- /client/src/components/Button-add-to-cart.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Fragment } from 'react'; 3 | import { 4 | Button 5 | } from 'reactstrap'; 6 | 7 | const propTypes = { 8 | addToCart: PropTypes.func.isRequired, 9 | sizeBtn: PropTypes.string.isRequired, 10 | infoItem: PropTypes.object.isRequired, 11 | selectedSize: PropTypes.string.isRequired, 12 | selectedColor: PropTypes.string.isRequired, 13 | toggleModal: PropTypes.func.isRequired, 14 | validateSizeSelection: PropTypes.func.isRequired, 15 | validateColorSelection: PropTypes.func.isRequired, 16 | colorSelectionMissingRemark: PropTypes.string.isRequired, 17 | sizeSelectionMissingRemark: PropTypes.string.isRequired 18 | }; 19 | 20 | const ButtonAddToCart = ({ 21 | addToCart, 22 | sizeBtn, 23 | infoItem, 24 | selectedSize, 25 | selectedColor, 26 | toggleModal, 27 | validateSizeSelection, 28 | validateColorSelection, 29 | colorSelectionMissingRemark, 30 | sizeSelectionMissingRemark 31 | }) => { 32 | 33 | const colorBtn = (colorSelectionMissingRemark.length > 0 || sizeSelectionMissingRemark.length > 0) ? 'danger' : 'success' 34 | 35 | return ( 36 | 37 | 42 | 43 | 44 | );}; 45 | 46 | ButtonAddToCart.propTypes = propTypes; 47 | 48 | export default ButtonAddToCart; 49 | 50 | -------------------------------------------------------------------------------- /client/src/reducers/cartReducer.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | ADD_TO_CART, 4 | DELETE_FROM_CART, 5 | DELETE_ALL_FROM_CART 6 | } from '../constants'; 7 | 8 | export const cartReducer = (state = [], action) => { 9 | 10 | const selectItemInCart = (state, action) => action !== undefined && state.filter(x => x.selectedSize === action.selectedSize && x.selectedColor === action.selectedColor && x._id === action._id)[0]; 11 | const cartWithoutItem = (state, action) => action !== undefined && state.filter(x => selectItemInCart(state, action) !== x); 12 | 13 | 14 | switch (action.type) { 15 | 16 | case ADD_TO_CART: 17 | return ( 18 | selectItemInCart(state, action.item) ? // Check if selected item through action (action.item) is already in cart array 19 | [...cartWithoutItem(state, action.item), {...selectItemInCart(state, action.item), quantity: selectItemInCart(state, action.item).quantity+1}] : // If Yes: return the cart array without the item to avoid duplicate. Then select the item by lookup the id of action (action.item) to match the id of cart array (state) to increment +1 to its quantity propriety. 20 | [...state, {...action.item, quantity: 1}] // If No: return cart array. Then add object of added item by concatenation, initialize its propriety quantity by 1. 21 | ); 22 | 23 | case DELETE_FROM_CART: 24 | return ( 25 | action.item.quantity === 1 ? 26 | [...cartWithoutItem(state, action.item)] : 27 | [...cartWithoutItem(state, action.item), {...action.item, quantity: selectItemInCart(state, action.item).quantity-1}] 28 | ); 29 | 30 | 31 | case DELETE_ALL_FROM_CART: 32 | return cartWithoutItem(state, action.item); 33 | 34 | 35 | default: 36 | return state; 37 | } 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /client/src/components/Button-sorter.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component, Fragment } from 'react' 3 | import { 4 | ButtonDropdown, 5 | DropdownToggle, 6 | DropdownMenu, 7 | DropdownItem 8 | } from 'reactstrap'; 9 | 10 | const propTypes = { 11 | dispatchToSortList: PropTypes.func.isRequired, 12 | sortArgsForFilter: PropTypes.string.isRequired, 13 | }; 14 | 15 | class ButtonSorter extends Component { 16 | 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | dropdownShowNumberItemsOpen: false, 21 | dropdownSortOpen: false 22 | }; 23 | } 24 | 25 | render() { 26 | const { dispatchToSortList, sortArgsForFilter } = this.props 27 | const { dropdownSortOpen } = this.state 28 | 29 | const dropDownWithArrow = (x) => 30 | x.indexOf("Asc") > 0 ? 31 | {' '+x} : 32 | {' '+x} 33 | 34 | 35 | const eachDropDown = (optionsArray) => optionsArray.map(x=>( 36 | dispatchToSortList(x)} key={x}> 37 | {dropDownWithArrow(x)} 38 | 39 | )) 40 | 41 | return ( 42 | this.setState({dropdownSortOpen: !dropdownSortOpen})}> 43 | 44 | Sort: {dropDownWithArrow(sortArgsForFilter)} 45 | 46 | 47 | {eachDropDown(['titleAsc', 'titleDesc', 'priceAsc', 'priceDesc'])} 48 | 49 | 50 | ); 51 | }; 52 | }; 53 | 54 | ButtonSorter.propTypes = propTypes; 55 | 56 | export default ButtonSorter; -------------------------------------------------------------------------------- /client/src/components/Checkout-modal.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import { 5 | Button, 6 | Modal, 7 | ModalHeader, 8 | ModalBody, 9 | ModalFooter, 10 | Col, 11 | Row 12 | } from 'reactstrap' 13 | 14 | const propTypes = { 15 | infoItem: PropTypes.object.isRequired, 16 | toggleModal: PropTypes.func.isRequired, 17 | selectedSize: PropTypes.string.isRequired, 18 | selectedColor: PropTypes.string.isRequired, 19 | openModal: PropTypes.bool.isRequired, 20 | totalItemsSelectorStats: PropTypes.number.isRequired 21 | }; 22 | 23 | const CheckoutModal = ({ 24 | infoItem, 25 | openModal, 26 | toggleModal, 27 | selectedSize, 28 | selectedColor, 29 | totalItemsSelectorStats 30 | }) => ( 31 | 32 | You have {totalItemsSelectorStats} item{totalItemsSelectorStats>1 && 's'} in your cart 33 | 34 | {totalItemsSelectorStats>1 && 'last item added:'} 35 | 36 | 37 | 38 | {infoItem.title} 39 |
{infoItem.price} $
40 |
color: {selectedColor}
41 |
size: {selectedSize}
42 | 43 |
44 |
45 | 46 | {' '} 47 | 48 | 49 |
50 | ); 51 | 52 | CheckoutModal.propTypes = propTypes; 53 | 54 | export default CheckoutModal; -------------------------------------------------------------------------------- /client/src/components/Checkout-mini-summary-preview.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Fragment } from 'react'; 3 | import { ListGroup, ListGroupItem, Col, Row, Button } from 'reactstrap'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | const propTypes = { 7 | getCart: PropTypes.array.isRequired, 8 | empty: PropTypes.bool.isRequired 9 | }; 10 | 11 | const CheckoutMiniSummaryPreview = ({ 12 | getCart, 13 | empty 14 | }) => { 15 | 16 | const styles = { 17 | fontSize: { 18 | fontSize: '13px' 19 | }, 20 | centerButtons: { 21 | display:'flex', 22 | justifyContent: 'center', 23 | margin: '10px', 24 | textDecoration: 'none' 25 | } 26 | }; 27 | 28 | const CardPreview = 29 | 30 | { 31 | !empty ? 32 | 33 | 34 | { 35 | getCart.map(x=> 36 | 37 |

x{x.quantity} {x.title} {x.price}$ size: {x.selectedSize} color: {x.selectedColor}

38 | 39 |
40 | 41 | ) 42 | } 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | : 52 | 53 | Empty card 54 | 55 | } 56 |
; 57 | 58 | 59 | return CardPreview; 60 | 61 | } 62 | 63 | CheckoutMiniSummaryPreview.propTypes = propTypes; 64 | 65 | export default CheckoutMiniSummaryPreview; 66 | 67 | -------------------------------------------------------------------------------- /client/src/selectors/selector_list_products_filter_sorter.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | const listToFilter = state => state.listFetchDataSuccess 4 | const argsForFilter = state => state.keywordsForFilter 5 | const sortForFilter = state => state.sortArgsForFilter 6 | const getSize = state => state.sortSizeForFilter 7 | const getPriceRange = state => state.reducerPriceRangeFilter 8 | 9 | const getFilteredList = (listFetchDataSuccess, keywordsForFilter, sortArgsForFilter, sortSizeForFilter, reducerPriceRangeFilter) => { 10 | 11 | const FilteredListByKeywords = listFetchDataSuccess.filter(x=> 12 | keywordsForFilter.some(el => x.tags.includes(el)) 13 | ) 14 | 15 | const FilteredListBySize = sortSizeForFilter === 'All' ? FilteredListByKeywords : 16 | FilteredListByKeywords.filter(y=> 17 | y.size.includes(sortSizeForFilter) 18 | ) 19 | 20 | const FilteredListByPriceRange = FilteredListBySize.filter(x => x.price <= reducerPriceRangeFilter) 21 | 22 | return FilteredListByPriceRange.sort((a, b) => { 23 | switch(sortArgsForFilter) { 24 | case 'titleAsc': 25 | if (a.title > b.title) return 1; 26 | if (a.title < b.title) return -1; 27 | 28 | case 'titleDesc': 29 | if (a.title < b.title) return 1; 30 | if (a.title > b.title) return -1; 31 | 32 | case 'priceAsc': 33 | if (a.price > b.price) return 1; 34 | if (a.price < b.price) return -1; 35 | 36 | case 'priceDesc': 37 | if (a.price < b.price) return 1; 38 | if (a.price > b.price) return -1; 39 | 40 | default: 41 | if (a.title > b.title) return 1; 42 | if (a.title < b.title) return -1; 43 | } 44 | }) 45 | } 46 | 47 | 48 | export const selectorListFilterSorter = createSelector( 49 | listToFilter, 50 | argsForFilter, 51 | sortForFilter, 52 | getSize, 53 | getPriceRange, 54 | getFilteredList 55 | ) 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /client/src/components/Item-list-filter-keywords.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react' 3 | import '../style/checkbox.min.css' 4 | 5 | const propTypes = { 6 | gender: PropTypes.string.isRequired, 7 | keywordsSelectAction: PropTypes.func.isRequired, 8 | categoriesProducts: PropTypes.object.isRequired, 9 | keywordsForFilter: PropTypes.array.isRequired 10 | }; 11 | 12 | class ItemsListFilterKeywords extends Component { 13 | 14 | constructor(props){ 15 | super(props); 16 | this.state = { 17 | cat: this.props.categoriesProductslength === 0 18 | } 19 | }; 20 | 21 | componentDidMount = () => { 22 | const { keywordsForFilter, actionFillFilters, categoriesProducts, gender } = this.props 23 | keywordsForFilter.length === 0 ? 24 | gender === 'men' ? (() => {actionFillFilters(categoriesProducts.men)})() : (() => {actionFillFilters(categoriesProducts.women)})() 25 | : console.log('gender err') 26 | }; 27 | 28 | render(){ 29 | 30 | const { 31 | categoriesProducts, 32 | keywordsSelectAction, 33 | keywordsForFilter, 34 | gender 35 | } = this.props; 36 | 37 | const cat = () => gender === 'men' ? categoriesProducts.men : categoriesProducts.women; 38 | 39 | const stateIncludesCategory = category => keywordsForFilter.includes(category); 40 | 41 | return ( 42 | cat().map(x=> 43 |
  • 44 |
    45 | {/* Empty onChange to avoid unrelevant msg error */} 46 | keywordsSelectAction(x)} checked={stateIncludesCategory(x)} onChange={()=>{}}/> 47 |
    48 | 49 |
    50 |
    51 |
  • 52 | ) 53 | ); 54 | 55 | }; 56 | }; 57 | 58 | ItemsListFilterKeywords.propTypes = propTypes; 59 | 60 | export default ItemsListFilterKeywords; 61 | 62 | -------------------------------------------------------------------------------- /client/src/containers/Item-container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { fetchItemApi } from '../actions/DataFetchingActions'; 3 | import { addToCart } from '../actions/CartActions'; 4 | import { connect } from 'react-redux'; 5 | import { selectorTotalItemsCart } from '../selectors/selector_list_statistics'; 6 | import Item from '../components/Item'; 7 | 8 | class ItemContainer extends Component { 9 | 10 | constructor(props){ 11 | super(props); 12 | this.state = { 13 | selectedSize: '', 14 | selectedColor: '', 15 | sizeSelectionMissingRemark: '', 16 | colorSelectionMissingRemark: '' 17 | } 18 | } 19 | 20 | componentDidMount = () => this.props.fetchItemApi(`/api/productsdata/${this.props.match.params.id}`); 21 | 22 | handleSizeSelection = selectedSize => this.setState({ selectedSize }); 23 | 24 | handleColorSelection = selectedColor => this.setState({ selectedColor }); 25 | 26 | validateSizeSelection = remark => remark === 'valid' ? this.setState({ sizeSelectionMissingRemark: '' }) : this.setState({ sizeSelectionMissingRemark: remark }); 27 | 28 | validateColorSelection = remark => remark === 'valid' ? this.setState({ colorSelectionMissingRemark: '' }) : this.setState({ colorSelectionMissingRemark: remark }); 29 | 30 | render = () => ; 31 | 32 | }; 33 | 34 | const mapStateToProps = state => ({ 35 | infoItem: state.itemFetchDataSuccess, 36 | loading: state.itemIsLoading, 37 | errorFetching: state.itemHasErrorm, 38 | totalItemsSelectorStats: selectorTotalItemsCart(state) 39 | }); 40 | 41 | const mapDispatchToProps = dispatch => ({ 42 | fetchItemApi: url => dispatch(fetchItemApi(url)), 43 | addToCart: x => dispatch(addToCart(x)) 44 | }); 45 | 46 | export default connect(mapStateToProps, mapDispatchToProps)(ItemContainer); 47 | 48 | -------------------------------------------------------------------------------- /client/src/components/Checkout-step-three.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react' 3 | import { 4 | Badge, 5 | Button, 6 | ListGroupItem, 7 | Collapse, 8 | Col, 9 | Row 10 | } from 'reactstrap'; 11 | 12 | const propTypes = { 13 | toggle: PropTypes.func.isRequired, 14 | step3: PropTypes.bool.isRequired, 15 | step3Unlock: PropTypes.bool.isRequired, 16 | getUserAddress: PropTypes.object.isRequired, 17 | onSubmitOrder: PropTypes.func.isRequired 18 | }; 19 | 20 | const CheckoutStepThree = ({ 21 | styles, 22 | step3, 23 | step3Unlock, 24 | toggle, 25 | getUserAddress, 26 | onSubmitOrder, 27 | selectorTotalAmountCart, 28 | totalDelivery 29 | }) => { 30 | 31 | const { 32 | address1, 33 | address2, 34 | city, 35 | country, 36 | firstName, 37 | lastName, 38 | phoneNumber, 39 | postalCode, 40 | province 41 | } = getUserAddress 42 | 43 | return ( 44 |
    45 | 46 |

    step3Unlock && toggle('step3')} > 47 | 3 Customer 48 |

    49 | 50 | 51 | 52 |

    Delivery address:

    53 |
    First Name: {firstName}
    54 |
    Last Name: {lastName}
    55 |
    Tel: {phoneNumber}
    56 |
    Country: {country}
    57 |
    City: {city}
    58 |
    State/Province: {province}
    59 |
    Postal Code: {postalCode}
    60 |
    Address: {address1 + ' ' + address2}
    61 | 62 | 63 |

    Payment:

    64 | 65 |
    66 |
    67 |
    68 |
    69 | ) 70 | } 71 | 72 | CheckoutStepThree.propTypes = propTypes; 73 | 74 | export default CheckoutStepThree 75 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 24 | React App 25 | 26 | 27 | 30 |
    31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /client/src/components/Each-item-in-list.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import StarRatings from 'react-star-ratings'; 5 | import { 6 | Col, 7 | Card, 8 | CardImg 9 | } from 'reactstrap'; 10 | 11 | const propTypes = { 12 | FilteredSortedList: PropTypes.array.isRequired, 13 | currentPage: PropTypes.number.isRequired, 14 | itemsMaxPage: PropTypes.number.isRequired 15 | }; 16 | 17 | const styles = { 18 | spaceColumn: { 19 | marginLeft: '25px', 20 | marginRight: '25px', 21 | marginBottom: '50px', 22 | border: 'none' 23 | }, 24 | fontSize: { 25 | fontSize: '15px' 26 | }, 27 | marginLeftBtn: { 28 | marginLeft: '30px' 29 | }, 30 | containerPaddingTop: { 31 | paddingTop: '35px' 32 | } 33 | }; 34 | 35 | const EachItemInList = ({ 36 | FilteredSortedList, 37 | currentPage, 38 | itemsMaxPage 39 | }) => { 40 | return (FilteredSortedList.slice((currentPage-1)*itemsMaxPage,itemsMaxPage*currentPage).map(x => 41 | 42 | 43 | 44 | 45 | 46 |
    47 |
    {x.color.map(x=>
    )}
    48 |

    {x.title}

    49 | 58 |

    {x.price} $

    59 |
    60 | 61 | 62 | )); 63 | }; 64 | 65 | EachItemInList.propTypes = propTypes; 66 | 67 | export default EachItemInList; 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /client/src/components/Admin-table-items.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import axios from 'axios'; 3 | import { Table } from 'reactstrap'; 4 | import AdminModalUpdate from './Admin-modal-update'; 5 | import AdminFormDeleteItem from './Admin-form-delete-item'; 6 | 7 | export default class Admin extends Component { 8 | constructor(props){ 9 | super(props); 10 | this.state={ 11 | apiList: [] 12 | } 13 | } 14 | 15 | async componentDidMount() { 16 | try { 17 | const response = await axios.get('/api/productsdata') 18 | const apiList = await response.data; 19 | this.setState({ apiList }) 20 | } catch (error) { 21 | console.log(error); 22 | } 23 | } 24 | 25 | render() { 26 | const stylesColor = (color) => ({ 27 | textDecoration: 'underline', 28 | textDecorationColor: color 29 | }) 30 | 31 | const { stylesTab2 } = this.props 32 | const { apiList } = this.state 33 | return ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | { 52 | apiList.map((x, index)=> 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ) 67 | } 68 | 69 |
    #TitleIdPriceColorsSizesTagsImagesDescriptionEditDelete
    {index+1}{x.title}{x._id}{x.price}${x.color.map(x=>{x} / )}{x.size.map(x=>x+' / ')}{x.tags}{x.images.length}{x.description.substring(0, 30)+'... '}
    70 | ) 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /client/src/components/Items-list-sidebar.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react' 3 | import ItemsListFilterKeywords from './Item-list-filter-keywords' 4 | import ItemsListFilterSize from './Items-list-filter-size' 5 | import ProductListFilterPriceBar from './Item-list-filter-priceBar' 6 | 7 | const propTypes = { 8 | gender: PropTypes.string.isRequired, 9 | dispatchSize: PropTypes.func.isRequired, 10 | sortSizeForFilter: PropTypes.string.isRequired, 11 | keywordsSelectAction: PropTypes.func.isRequired, 12 | keywordsForFilter: PropTypes.array.isRequired, 13 | categoriesProducts: PropTypes.object.isRequired, 14 | actionPriceRangeFilter: PropTypes.func.isRequired, 15 | reducerPriceRangeFilter: PropTypes.number.isRequired, 16 | oneKeywordForFilter: PropTypes.func.isRequired, 17 | actionFillFilters: PropTypes.func.isRequired 18 | }; 19 | 20 | const styles = { 21 | subTitles:{ 22 | color: 'grey', 23 | marginTop:'20px' 24 | } 25 | } 26 | 27 | const ItemsListSidebar = ({ 28 | gender, 29 | dispatchSize, 30 | sortSizeForFilter, 31 | keywordsSelectAction, 32 | keywordsForFilter, 33 | categoriesProducts, 34 | actionPriceRangeFilter, 35 | reducerPriceRangeFilter, 36 | oneKeywordForFilter, 37 | actionFillFilters 38 | }) => ( 39 |
    40 |

    Categories

    41 | 49 |

    Size

    50 | 54 |

    Price {`< ${reducerPriceRangeFilter}$`}

    55 | 59 |
    60 | ) 61 | 62 | ItemsListSidebar.propTypes = propTypes; 63 | 64 | export default ItemsListSidebar; 65 | 66 | -------------------------------------------------------------------------------- /client/src/components/Submenu.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { 4 | Row, 5 | Col, 6 | } from 'reactstrap'; 7 | import { Link } from 'react-router-dom'; 8 | 9 | const propTypes = { 10 | sendOneKeyword: PropTypes.func.isRequired, 11 | gender: PropTypes.string.isRequired, 12 | itemsListByGender: PropTypes.array.isRequired, 13 | handleSubMenuExit: PropTypes.func.isRequired 14 | } 15 | 16 | 17 | const styles = { 18 | subMenu: { 19 | width: '100%', 20 | height: '250px', 21 | backgroundColor:'rgba(255, 255, 255, 0.9)', 22 | position: 'absolute', 23 | top:'0', 24 | left:'0', 25 | zIndex:'2' 26 | }, 27 | subMenuImage: { 28 | width: '100%', 29 | maxHeight: '300px', 30 | padding:'70px' 31 | }, 32 | subMenuCategories: { 33 | paddingTop:'70px' 34 | }, 35 | subMenuCategoriesUl: { 36 | listStyleType: 'none', 37 | fontSize: '15px' 38 | }, 39 | subMenuCategory: { 40 | color: '#343a40' 41 | } 42 | } 43 | 44 | const Submenu = ({ 45 | sendOneKeyword, 46 | gender, 47 | itemsListByGender, 48 | handleSubMenuExit 49 | }) => ( 50 |
    51 | 52 | 53 | {gender} 56 | 57 | 58 |

    Categories

    59 |
    60 | { 61 | itemsListByGender.map(x =>
    sendOneKeyword(x)}> {x}
    ) 62 | } 63 |
    64 | 65 |
    66 |
    67 | ); 68 | 69 | Submenu.propTypes = propTypes; 70 | 71 | export default Submenu; 72 | -------------------------------------------------------------------------------- /client/src/containers/Items-list-container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | sortArgsForFilter, 5 | keywordsForFilter, 6 | actionSizeForFilter, 7 | oneKeywordForFilter, 8 | fetchDataApi, 9 | actionPriceRangeFilter, 10 | actionFillFilters 11 | } from '../actions/DataFetchingActions'; 12 | import { selectorListFilterSorter } from '../selectors/selector_list_products_filter_sorter'; 13 | import ItemsList from '../components/Items-list'; 14 | 15 | class ProductsListContainer extends Component { 16 | 17 | constructor(props){ 18 | super(props); 19 | this.state = { 20 | currentPage: 1, 21 | itemsMaxPage: 9 22 | }; 23 | }; 24 | 25 | componentDidMount = () => this.props.fetchDataApi('/api/productsdata'); 26 | 27 | 28 | currentPageHandler = x => { 29 | const { currentPage } = this.state; 30 | x === 'next' ? this.setState({currentPage: currentPage + 1}) : 31 | x === 'previous' ? this.setState({currentPage: currentPage - 1} ) : 32 | x === 'empty' ? this.setState({currentPage: 1}) : 33 | Number.isInteger(x) && this.setState({currentPage: x + 1}); 34 | }; 35 | 36 | render = () => 41 | 42 | } 43 | 44 | const mapStateToProps = state => { 45 | const { keywordsForFilter, sortSizeForFilter, categoriesProducts, listIsLoading, reducerPriceRangeFilter,sortArgsForFilter } = state; 46 | return { 47 | listIsLoading, 48 | FilteredSortedList: selectorListFilterSorter(state), 49 | reducerPriceRangeFilter, 50 | keywordsForFilter, 51 | sortSizeForFilter, 52 | sortArgsForFilter, 53 | categoriesProducts 54 | } 55 | }; 56 | 57 | const mapDispatchToProps = dispatch => ({ 58 | fetchDataApi: url => dispatch(fetchDataApi(url)), 59 | oneKeywordForFilter: x => dispatch(oneKeywordForFilter(x)), 60 | actionFillFilters: x => dispatch(actionFillFilters(x)), 61 | dispatchSize: x => dispatch(actionSizeForFilter(x)), 62 | actionPriceRangeFilter: x => dispatch(actionPriceRangeFilter(x)), 63 | dispatchToSortList: x => dispatch(sortArgsForFilter(x)), 64 | keywordsSelectAction: x => dispatch(keywordsForFilter(x)) 65 | }); 66 | 67 | export default connect(mapStateToProps, mapDispatchToProps)(ProductsListContainer); 68 | 69 | -------------------------------------------------------------------------------- /client/src/Router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | import Empty from './components/Empty'; 4 | import Loadable from 'react-loadable'; 5 | import NavbarContainer from './containers/Navbar-container'; 6 | import Footer from './components/Footer'; 7 | import './style/transition.css'; 8 | 9 | 10 | const Loading = () =>
    ; 11 | 12 | const ItemContainer = Loadable({ 13 | loader: () => import('./containers/Item-container'), 14 | loading: Loading 15 | }); 16 | 17 | const CheckoutContainer = Loadable({ 18 | loader: () => import('./containers/Checkout-container'), 19 | loading: Loading 20 | }); 21 | 22 | const CartContainer = Loadable({ 23 | loader: () => import('./containers/Cart-container'), 24 | loading: Loading 25 | }); 26 | 27 | const HomepageContainer = Loadable({ 28 | loader: () => import('./containers/Homepage-container'), 29 | loading: Loading 30 | }); 31 | 32 | const ItemsListContainer = Loadable({ 33 | loader: () => import('./containers/Items-list-container'), 34 | loading: Loading 35 | }); 36 | 37 | const ItemsListGenderHomepage = Loadable({ 38 | loader: () => import('./components/Items-list-gender-homepage'), 39 | loading: Loading 40 | }); 41 | 42 | const AdminContainer = Loadable({ 43 | loader: () => import('./containers/Admin-container'), 44 | loading: Loading 45 | }); 46 | 47 | const Secret = Loadable({ 48 | loader: () => import('./components/Secret'), 49 | loading: Loading 50 | }); 51 | 52 | const Router = () => ( 53 |
    54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
    68 |
    69 | ); 70 | 71 | export default Router; 72 | -------------------------------------------------------------------------------- /client/src/components/Items-list-banner.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react' 3 | import { 4 | Jumbotron, 5 | Container, 6 | } from 'reactstrap'; 7 | import { 8 | isMobile 9 | } from "react-device-detect"; 10 | import ItemsListHtagsLabels from './Items-list-htags-labels' 11 | 12 | const propTypes = { 13 | gender: PropTypes.string, 14 | reducerPriceRangeFilter: PropTypes.number, 15 | sortArgsForFilter: PropTypes.string, 16 | sortSizeForFilter: PropTypes.string, 17 | keywordsForFilter: PropTypes.array, 18 | } 19 | 20 | const styles={ 21 | bannerCoverMenPc: { 22 | backgroundImage: 'url("/images/banner-men-pc.jpg")', 23 | backgroundSize: 'cover' 24 | }, 25 | bannerCoverWomenPc: { 26 | backgroundImage: 'url("/images/banner-women-pc.jpg")', 27 | backgroundSize: 'cover' 28 | }, 29 | bannerCoverMenMobile: { 30 | backgroundImage: 'url("/images/banner-men-mobile.jpg")', 31 | backgroundSize: 'cover' 32 | }, 33 | bannerCoverWomenMobile: { 34 | backgroundImage: 'url("/images/banner-women-mobile.jpg")', 35 | backgroundSize: 'cover' 36 | }, 37 | textBanner: { 38 | textShadow: "3px 3px 3px grey", 39 | color:'white' 40 | }, 41 | titleH1Pc: { 42 | fontSize: '80px' 43 | } 44 | } 45 | 46 | const ItemsListBanner = ({ 47 | gender, 48 | reducerPriceRangeFilter, 49 | sortArgsForFilter, 50 | sortSizeForFilter, 51 | keywordsForFilter 52 | }) => { 53 | 54 | const { bannerCoverMenPc, bannerCoverMenMobile, bannerCoverWomenMobile, bannerCoverWomenPc, textBanner, titleH1Pc } = styles 55 | const backgroundJumbotron = 56 | isMobile && gender === 'men' ? bannerCoverMenMobile : 57 | isMobile && gender === 'women' ? bannerCoverWomenMobile : 58 | isMobile === false && gender === 'men' ? bannerCoverMenPc : 59 | bannerCoverWomenPc 60 | 61 | return ( 62 | 63 | 64 |

    {gender === 'men' ? 'Men' : 'Women' }

    65 | 71 |
    72 |
    73 | ) 74 | }; 75 | 76 | ItemsListBanner.propTypes = propTypes; 77 | 78 | export default ItemsListBanner; -------------------------------------------------------------------------------- /client/public/images/loader.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/components/Admin-table-orders.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import axios from 'axios'; 3 | import { Table } from 'reactstrap'; 4 | 5 | export default class AdminTableOrders extends Component { 6 | constructor(props){ 7 | super(props); 8 | this.state={ 9 | apiList: [] 10 | } 11 | } 12 | 13 | async componentDidMount() { 14 | try { 15 | const response = await axios.get('/api/orders') 16 | const apiList = await response.data; 17 | this.setState({ apiList }) 18 | } catch (error) { 19 | console.log(error); 20 | } 21 | } 22 | 23 | render() { 24 | const stylesColor = (color) => ({ 25 | textDecoration: 'underline', 26 | textDecorationColor: color 27 | }) 28 | const { stylesTab1 } = this.props 29 | const { apiList } = this.state 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | { 54 | apiList.map((x, index)=> 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | ) 74 | } 75 | 76 |
    #IdRefDateOrderOrder AmountDelivery AmountEmailLast NameFirst NameCountryCityProvincePostal CodePhone NumberAddress
    {index+1}{x._id}{x.ref}{x.createdAt}{x.order.map(item=>
    {`x${item.quantity} ${item.idItem}(${item.titleItem}[${item.selectedSize}, ${item.selectedColor}] ${item.price}$) `}
    )}
    {x.totalAmount+ ' $'}{x.totalDelivery+ ' $'}{x.customerinfo.email}{x.customerinfo.lastName}{x.customerinfo.firstName}{x.customerinfo.country}{x.customerinfo.city}{x.customerinfo.province}{x.customerinfo.postalCode}{x.customerinfo.phoneNumber}{x.customerinfo.address1 + ' ' +x.customerinfo.address2}
    77 | ) 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /client/src/components/Checkout-step-one.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react' 3 | import { 4 | Row, 5 | Col, 6 | Input, 7 | Badge, 8 | Button, 9 | ListGroupItem, 10 | Collapse, 11 | Label, 12 | CustomInput, 13 | FormFeedback 14 | } from 'reactstrap'; 15 | import { Link } from 'react-router-dom'; 16 | 17 | const propTypes = { 18 | email: PropTypes.string.isRequired, 19 | step1: PropTypes.bool.isRequired, 20 | toggle: PropTypes.func.isRequired, 21 | stepsUnlock: PropTypes.func.isRequired, 22 | onChangeEmail: PropTypes.func.isRequired, 23 | emailIsValid: PropTypes.bool.isRequired, 24 | handleEmailValidation: PropTypes.func.isRequired, 25 | emailIsValid: PropTypes.bool.isRequired, 26 | }; 27 | 28 | const CheckoutStepOne = ({ 29 | styles, 30 | email, 31 | step1, 32 | toggle, 33 | stepsUnlock, 34 | emailIsValid, 35 | handleEmailValidation, 36 | onChangeEmail 37 | }) => { 38 | const emailValidation = (/^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i).test(String(email).toLowerCase()) 39 | return ( 40 | 41 |

    toggle('step1')} > 42 | 1 Customer 43 |

    44 | 45 |

    Enter your email for express checkout, an email will be sent to you allowing account creation

    46 | 47 | 48 | 49 | 58 | 59 | Email is not valid 60 | 61 | 62 | 63 | 71 | 72 | 73 |
    74 | 75 |
    76 | 77 |
    78 |
    79 |
    80 | ) 81 | } 82 | 83 | CheckoutStepOne.propTypes = propTypes; 84 | 85 | export default CheckoutStepOne 86 | -------------------------------------------------------------------------------- /client/src/components/Button-filter-mobile.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import { Button, Modal, ModalHeader, ModalBody } from 'reactstrap'; 4 | import ItemsListFilterKeywords from './Item-list-filter-keywords' 5 | import ItemsListFilterSize from './Items-list-filter-size' 6 | import ItemListFilterPriceBar from './Item-list-filter-priceBar' 7 | 8 | const propTypes = { 9 | dispatchSize: PropTypes.func.isRequired, 10 | gender: PropTypes.string.isRequired, 11 | sortSizeForFilter: PropTypes.string.isRequired, 12 | keywordsSelectAction: PropTypes.func.isRequired, 13 | categoriesProducts: PropTypes.object.isRequired, 14 | keywordsForFilter: PropTypes.array.isRequired, 15 | actionPriceRangeFilter: PropTypes.func.isRequired, 16 | reducerPriceRangeFilter: PropTypes.number.isRequired, 17 | listLength: PropTypes.number.isRequired 18 | }; 19 | 20 | class ButtonFilterMobile extends Component { 21 | 22 | constructor(props) { 23 | super(props); 24 | this.state = { 25 | modal: false 26 | }; 27 | }; 28 | 29 | toggle = () => { 30 | this.setState({ 31 | modal: !this.state.modal 32 | }); 33 | }; 34 | 35 | render() { 36 | 37 | const { 38 | dispatchSize, 39 | gender, 40 | sortSizeForFilter, 41 | keywordsSelectAction, 42 | categoriesProducts, 43 | keywordsForFilter, 44 | actionPriceRangeFilter, 45 | reducerPriceRangeFilter, 46 | listLength 47 | } = this.props; 48 | 49 | 50 | return ( 51 |
    52 | 53 | 54 | Results: {listLength} 55 | 56 |

    Categories

    57 | 63 |

    Size

    64 | 68 |

    Price {`< ${reducerPriceRangeFilter}$`}

    69 | 73 |
    74 |
    75 |
    76 | ); 77 | } 78 | } 79 | 80 | ButtonFilterMobile.propTypes = propTypes; 81 | 82 | export default ButtonFilterMobile; -------------------------------------------------------------------------------- /client/src/components/Admin.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import axios from 'axios'; 3 | import AdminTablesItems from './Admin-table-items' 4 | import AdminFormAddItem from './Admin-form-add-item' 5 | import AdminHistoryLog from './Admin-history-log' 6 | import AdminTableOrders from './Admin-table-orders' 7 | import { TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap'; 8 | 9 | export default class Admin extends Component { 10 | constructor(props){ 11 | super(props); 12 | this.state={ 13 | apiList: [], 14 | activeTab: '2' 15 | } 16 | } 17 | 18 | componentDidMount = async () => { 19 | try { 20 | const response = await axios.get('/api/productsdata') 21 | const apiList = await response.data; 22 | this.setState({ apiList }) 23 | } catch (error) { 24 | console.log(error); 25 | } 26 | } 27 | 28 | toggle = tab => this.state.activeTab !== tab && this.setState({ activeTab: tab }); 29 | 30 | render() { 31 | 32 | const styles = { 33 | tab1: { 34 | cursor: 'pointer', 35 | backgroundColor: '#cd5957', 36 | color:'white' 37 | }, 38 | tab2: { 39 | cursor: 'pointer', 40 | backgroundColor: '#78a4a2', 41 | color:'white' 42 | }, 43 | tab3: { 44 | cursor: 'pointer', 45 | backgroundColor: '#66bceb', 46 | color:'white' 47 | }, 48 | tab4: { 49 | cursor: 'pointer', 50 | backgroundColor: '#ffce56', 51 | color:'white' 52 | }, 53 | 54 | } 55 | 56 | return ( 57 |
    58 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 |
    95 | ) 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /client/src/reducers/listFetchReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | LIST_ERROR, 3 | LIST_IS_LOADING, 4 | LIST_FETCH_SUCCESS, 5 | FILTER_CATEGORIES_MULTIPLE_KEYWORDS, 6 | FILTER_CATEGORIES_ONE_KEYWORD, 7 | FILTER_PRICE_RANGE, 8 | FILTER_SIZE, 9 | RESET_KEYWORDS, 10 | ITEM_ERROR, 11 | ITEM_IS_LOADING, 12 | FILTER_ARGS, 13 | FILL_FILTER, 14 | ITEM_FETCH_SUCCESS 15 | } from '../constants.js'; 16 | 17 | export const listHasError = (state = false, action) => { 18 | switch (action.type) { 19 | case LIST_ERROR: 20 | return action.hasErrored; 21 | 22 | default: 23 | return state; 24 | } 25 | }; 26 | 27 | export const itemHasError = (state = false, action) => { 28 | switch (action.type) { 29 | case ITEM_ERROR: 30 | return action.hasErrored; 31 | 32 | default: 33 | return state; 34 | } 35 | }; 36 | 37 | export const listIsLoading = (state = false, action) => { 38 | switch (action.type) { 39 | case LIST_IS_LOADING: 40 | return action.isLoading; 41 | 42 | default: 43 | return state; 44 | } 45 | }; 46 | 47 | export const itemIsLoading = (state = false, action) => { 48 | switch (action.type) { 49 | case ITEM_IS_LOADING: 50 | return action.isLoading; 51 | 52 | default: 53 | return state; 54 | } 55 | }; 56 | 57 | export const sortArgsForFilter = (state = 'titleAsc', action) => { 58 | switch (action.type) { 59 | case FILTER_ARGS: 60 | return action.sortArg; 61 | 62 | default: 63 | return state; 64 | } 65 | }; 66 | 67 | export const sortSizeForFilter = (state = 'All', action) => { 68 | switch (action.type) { 69 | case FILTER_SIZE: 70 | return action.getSize; 71 | 72 | default: 73 | return state; 74 | } 75 | }; 76 | 77 | export const reducerPriceRangeFilter = (state = 200, action) => { 78 | switch (action.type) { 79 | case FILTER_PRICE_RANGE: 80 | return action.getPriceRange; 81 | 82 | default: 83 | return state; 84 | } 85 | }; 86 | 87 | export const keywordsForFilter = (state = [], action) => { 88 | switch (action.type) { 89 | 90 | case FILTER_CATEGORIES_MULTIPLE_KEYWORDS: 91 | return (state.includes(action.keywords) ? 92 | state = state.filter(x => x !== action.keywords) : 93 | state = [...state, action.keywords]) 94 | 95 | case FILTER_CATEGORIES_ONE_KEYWORD: 96 | return state = [action.keywords] 97 | 98 | case FILL_FILTER: 99 | return state = action.keywords 100 | 101 | case RESET_KEYWORDS: 102 | return state = [] 103 | 104 | default: 105 | return state; 106 | } 107 | }; 108 | 109 | export const listFetchDataSuccess = (state = [], action) => { 110 | switch (action.type) { 111 | case LIST_FETCH_SUCCESS: 112 | return action.items; 113 | 114 | default: 115 | return state; 116 | } 117 | }; 118 | 119 | export const itemFetchDataSuccess = (state = [], action) => { 120 | switch (action.type) { 121 | case ITEM_FETCH_SUCCESS: 122 | return action.item; 123 | 124 | default: 125 | return state; 126 | } 127 | }; 128 | -------------------------------------------------------------------------------- /client/src/style/rangeslider.min.css: -------------------------------------------------------------------------------- 1 | .rangeslider{margin:20px 0;position:relative;background:#e6e6e6;-ms-touch-action:none;touch-action:none}.rangeslider,.rangeslider .rangeslider__fill{display:block;box-shadow:inset 0 1px 3px rgba(0,0,0,.4)}.rangeslider .rangeslider__handle{background:#fff;border:1px solid #ccc;cursor:pointer;display:inline-block;position:absolute;box-shadow:0 1px 3px rgba(0,0,0,.4),0 -1px 3px rgba(0,0,0,.4)}.rangeslider .rangeslider__handle .rangeslider__active{opacity:1}.rangeslider .rangeslider__handle-tooltip{width:40px;height:40px;text-align:center;position:absolute;background-color:rgba(0,0,0,.8);font-weight:400;font-size:14px;transition:all .1s ease-in;border-radius:4px;display:inline-block;color:#fff;left:50%;transform:translate3d(-50%,0,0)}.rangeslider .rangeslider__handle-tooltip span{margin-top:12px;display:inline-block;line-height:100%}.rangeslider .rangeslider__handle-tooltip:after{content:' ';position:absolute;width:0;height:0}.rangeslider-horizontal{height:12px;border-radius:10px}.rangeslider-horizontal .rangeslider__fill{height:100%;background-color:#7cb342;border-radius:10px;top:0}.rangeslider-horizontal .rangeslider__handle{width:30px;height:30px;border-radius:30px;top:50%;transform:translate3d(-50%,-50%,0)}.rangeslider-horizontal .rangeslider__handle:after{content:' ';position:absolute;width:16px;height:16px;top:6px;left:6px;border-radius:50%;background-color:#dadada;box-shadow:0 1px 3px rgba(0,0,0,.4) inset,0 -1px 3px rgba(0,0,0,.4) inset}.rangeslider-horizontal .rangeslider__handle-tooltip{top:-55px}.rangeslider-horizontal .rangeslider__handle-tooltip:after{border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid rgba(0,0,0,.8);left:50%;bottom:-8px;transform:translate3d(-50%,0,0)}.rangeslider-vertical{margin:20px auto;height:150px;max-width:10px;background-color:transparent}.rangeslider-vertical .rangeslider__fill,.rangeslider-vertical .rangeslider__handle{position:absolute}.rangeslider-vertical .rangeslider__fill{width:100%;background-color:#7cb342;box-shadow:none;bottom:0}.rangeslider-vertical .rangeslider__handle{width:30px;height:10px;left:-10px;box-shadow:none}.rangeslider-vertical .rangeslider__handle-tooltip{left:-100%;top:50%;transform:translate3d(-50%,-50%,0)}.rangeslider-vertical .rangeslider__handle-tooltip:after{border-top:8px solid transparent;border-bottom:8px solid transparent;border-left:8px solid rgba(0,0,0,.8);left:100%;top:12px}.rangeslider-reverse.rangeslider-horizontal .rangeslider__fill{right:0}.rangeslider-reverse.rangeslider-vertical .rangeslider__fill{top:0;bottom:inherit}.rangeslider__labels{position:relative}.rangeslider-vertical .rangeslider__labels{position:relative;list-style-type:none;margin:0 0 0 24px;padding:0;text-align:left;width:250px;height:100%;left:10px}.rangeslider-vertical .rangeslider__labels .rangeslider__label-item{position:absolute;transform:translate3d(0,-50%,0)}.rangeslider-vertical .rangeslider__labels .rangeslider__label-item::before{content:'';width:10px;height:2px;background:#000;position:absolute;left:-14px;top:50%;transform:translateY(-50%);z-index:-1}.rangeslider__labels .rangeslider__label-item{position:absolute;font-size:14px;cursor:pointer;display:inline-block;top:10px;transform:translate3d(-50%,0,0)} -------------------------------------------------------------------------------- /client/src/actions/DataFetchingActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | LIST_ERROR, 3 | LIST_IS_LOADING, 4 | LIST_FETCH_SUCCESS, 5 | FILTER_ARGS, 6 | FILTER_CATEGORIES_MULTIPLE_KEYWORDS, 7 | FILTER_CATEGORIES_ONE_KEYWORD, 8 | FILTER_SIZE, 9 | FILTER_PRICE_RANGE, 10 | RESET_KEYWORDS, 11 | ITEM_ERROR, 12 | ITEM_IS_LOADING, 13 | FILL_FILTER, 14 | ITEM_FETCH_SUCCESS 15 | } from '../constants'; 16 | 17 | export const listHasError = bool => ({ 18 | type: LIST_ERROR, 19 | hasErrored: bool 20 | }); 21 | 22 | export const itemHasError = bool => ({ 23 | type: ITEM_ERROR, 24 | hasErrored: bool 25 | }); 26 | 27 | export const listIsLoading = bool => ({ 28 | type: LIST_IS_LOADING, 29 | isLoading: bool 30 | }); 31 | 32 | export const itemIsLoading = bool => ({ 33 | type: ITEM_IS_LOADING, 34 | isLoading: bool 35 | }); 36 | 37 | export const listFetchDataSuccess = items => ({ 38 | type: LIST_FETCH_SUCCESS, 39 | items 40 | }); 41 | 42 | export const listFilteredByKeywords = items => ({ 43 | type: LIST_FETCH_SUCCESS, 44 | items 45 | }); 46 | 47 | export const actionSizeForFilter = getSize => ({ 48 | type: FILTER_SIZE, 49 | getSize 50 | }); 51 | 52 | export const actionPriceRangeFilter = getPriceRange => ({ 53 | type: FILTER_PRICE_RANGE, 54 | getPriceRange 55 | }); 56 | 57 | export const sortArgsForFilter = sortArg => ({ 58 | type: FILTER_ARGS, 59 | sortArg 60 | }); 61 | 62 | export const keywordsForFilter = keywords => ({ 63 | type: FILTER_CATEGORIES_MULTIPLE_KEYWORDS, 64 | keywords 65 | }); 66 | 67 | export const oneKeywordForFilter = keywords => ({ 68 | type: FILTER_CATEGORIES_ONE_KEYWORD, 69 | keywords 70 | }); 71 | 72 | export const actionFillFilters = keywords => ({ 73 | type: FILL_FILTER, 74 | keywords 75 | }); 76 | 77 | export const resetKeywords = () => ({ type: RESET_KEYWORDS }); 78 | 79 | export const itemFetchDataSuccess = item => ({ 80 | type: ITEM_FETCH_SUCCESS, 81 | item 82 | }); 83 | 84 | export const fetchDataApi = url => { 85 | return dispatch => { 86 | dispatch(listIsLoading(true)); 87 | fetch(url) 88 | .then(response => { 89 | if (!response.ok) { 90 | throw Error(response.statusText); 91 | } 92 | return response; 93 | }) 94 | .then(response => response.json()) 95 | .then(items => dispatch(listIsLoading(false)) && dispatch(listFetchDataSuccess(items))) 96 | .catch(() => dispatch(listHasError(true))); 97 | } 98 | }; 99 | 100 | export const fetchItemApi = url => { 101 | return dispatch => { 102 | dispatch(itemIsLoading(true)) 103 | fetch(url) 104 | .then(response => { 105 | if (!response.ok) { 106 | throw Error(response.statusText); 107 | } 108 | return response; 109 | }) 110 | .then(response => response.json()) 111 | .then(items => dispatch(itemFetchDataSuccess(items))) 112 | .then(() => dispatch(itemIsLoading(false))) 113 | .catch(() => dispatch(itemHasError(true))); 114 | } 115 | }; -------------------------------------------------------------------------------- /client/src/containers/Admin-container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import axios from 'axios'; 3 | import { Link, Redirect } from "react-router-dom"; 4 | import { Button, Form, FormGroup, Label, Input, Container, Col } from 'reactstrap'; 5 | 6 | class AdminContainer extends Component { 7 | 8 | constructor(props){ 9 | super(props); 10 | this.state = { 11 | email: 'admin', 12 | password: 'admin', 13 | apiResponse: '' 14 | }; 15 | } 16 | 17 | onChangeEmail = (e) => { 18 | this.setState({email: e.target.value}) 19 | } 20 | 21 | onChangePassword = (e) => { 22 | this.setState({password: e.target.value}) 23 | } 24 | 25 | onLogin = async() => { 26 | try { 27 | const req = axios.post('/api/signin', { 28 | email: this.state.email, 29 | password: this.state.password 30 | }) 31 | const response = await req; 32 | localStorage.setItem("token", response.data.token); 33 | this.setState({apiResponse: 'success'}) 34 | console.log(response) 35 | } catch (error) { 36 | this.setState({ apiResponse: error.response.statusText}) 37 | console.log(error.response); 38 | } 39 | 40 | } 41 | 42 | onSignup = () => { 43 | axios.post('/api/signup', { 44 | email: this.state.email, 45 | password: this.state.password 46 | }) 47 | .then(function (response) { 48 | console.log(response); 49 | }) 50 | .catch(function (error) { 51 | console.log(error.response.data.error); 52 | }); 53 | } 54 | 55 | redirectLoginSuccessListener = () => { 56 | if (this.state.apiResponse === 'success') { 57 | return 62 | 63 | } 64 | } 65 | 66 | render() { 67 | const errors = 68 | this.state.apiResponse === 'Bad Request' ? 69 | 'Please fill the email and password fields' : 70 | this.state.apiResponse === 'Unauthorized' && 71 | 'Email or password incorrect' 72 | 73 | return ( 74 |
    75 | {this.redirectLoginSuccessListener()} 76 | 77 |

    Admin Dashboard

    78 |
    79 | 80 | 81 | 82 | 89 | 90 | 91 | 92 | 93 | 94 | 101 | 102 | 103 | 104 | secret page 105 |

    {errors}

    106 |
    107 |
    108 |
    109 | ) 110 | 111 | } 112 | } 113 | 114 | export default AdminContainer; -------------------------------------------------------------------------------- /client/src/components/Admin-form-add-item.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Input, Form, FormGroup, Label, Container, Alert } from 'reactstrap'; 3 | import axios from 'axios'; 4 | 5 | class AdminFormAddItem extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | modalEdit: false, 10 | title: 'tteeest', 11 | color: 'blue, red', 12 | size: 'XS, L', 13 | tags: 'Polos', 14 | images: 'https://i.ytimg.com/vi/Bor5lkRyeGo/hqdefault.jpg, https://i.ytimg.com/vi/Bor5lkRyeGo/hqdefault.jpg', 15 | description: '', 16 | price: 0, 17 | success: false 18 | }; 19 | } 20 | 21 | toggle = () => { 22 | this.setState({ 23 | modalEdit: !this.state.modalEdit 24 | }); 25 | } 26 | 27 | onSubmit = (title, price, color, size, tags, images, description) => { 28 | axios.post('/api/add/item', { 29 | title, 30 | price, 31 | color: (color.slice(0)+'').replace(/\s/g,'').split(','), 32 | size: (size.slice(0)+'').replace(/\s/g,'').split(','), 33 | tags: (tags.slice(0)+'').replace(/\s/g,'').split(','), 34 | images: (images.slice(0)+'').replace(/\s/g,'').split(','), 35 | description 36 | }) 37 | .then(() => { 38 | window.location.reload(true) 39 | }) 40 | .catch(function (error) { 41 | console.log(error); 42 | }); 43 | } 44 | 45 | onChangeTitle = (e) => this.setState({title: e.target.value}) 46 | onChangePrice = (e) => this.setState({price: e.target.value}) 47 | onChangeColor = (e) => this.setState({color: [e.target.value]}) 48 | onChangesize = (e) => this.setState({size: e.target.value}) 49 | onChangeTags = (e) => this.setState({tags: [e.target.value]}) 50 | onChangeImages = (e) => this.setState({images: [e.target.value]}) 51 | onChangeDescription = (e) => this.setState({ description: e.target.value }) 52 | 53 | 54 | render() { 55 | const { title, price, color, size, tags, images, description } = this.state 56 | return ( 57 | 58 |

    Add new item

    59 |
    60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
    89 | 98 |
    99 | ); 100 | } 101 | } 102 | 103 | export default AdminFormAddItem; -------------------------------------------------------------------------------- /routes/router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const passport = require('passport'); 3 | const Authentication = require('../controllers/authentication'); 4 | const router = express.Router(); 5 | const ModelProducts = require('../models/ModelProducts'); 6 | const ModelLog = require('../models/ModelLog'); 7 | const ModelOrders = require('../models/ModelOrders'); 8 | 9 | router.get('/productsdata', (req, res)=> { 10 | ModelProducts.find((err, x) => { 11 | err ? res.status(500).send(err) : 12 | res.json(x) 13 | }) 14 | }) 15 | 16 | router.get('/productsdata/:id', (req, res)=> { 17 | ModelProducts.findById(req.params.id) 18 | .then(x => { 19 | if (!x) return res.status(404).end() 20 | return res.json(x) 21 | }) 22 | .catch(err => next(err)) 23 | }) 24 | 25 | router.put('/update/item', (req, res, next)=> { 26 | const { id, title, price, color, size, tags, images, description } = req.body 27 | ModelProducts.findById(id, (err, model) => { 28 | if (err) return handleError(err); 29 | 30 | model.set({ title, price, color, size, tags, images, description }); 31 | model.save(function (err, updatedItem) { 32 | if (err) return console.log(err); 33 | next(); 34 | }); 35 | }).then(()=>{ 36 | const logUpdate = new ModelLog({ 37 | type: 'Update', 38 | time: new Date(), 39 | itemid: id, 40 | itemtitle: title 41 | }); 42 | logUpdate.save(function(err, logSaved){ 43 | if(err){return next(err);} 44 | res.end(); 45 | }); 46 | }).catch(err => next(err)); 47 | }) 48 | 49 | router.post('/add/item', (req, res, next)=> { 50 | const { title, price, description, size, tags, images, color } = req.body 51 | const newItem = new ModelProducts({ 52 | title, 53 | price, 54 | color, 55 | size, 56 | tags, 57 | images, 58 | description 59 | }); 60 | newItem.save((err, saveditem) => { 61 | if (err) return console.log(err); 62 | next(); 63 | }) 64 | const logAdd = new ModelLog({ 65 | type: 'Create', 66 | time: new Date(), 67 | itemid: newItem._id, 68 | itemtitle: title 69 | }); 70 | logAdd.save(function(err, logSaved){ 71 | if(err){return next(err);} 72 | res.end(); 73 | }); 74 | }) 75 | 76 | router.post('/add/orders', (req, res, next)=> { 77 | const { ref, customerinfo, order, totalDelivery, totalAmount } = req.body 78 | const newOrder = new ModelOrders({ 79 | ref, 80 | customerinfo, 81 | order, 82 | totalDelivery, 83 | totalAmount 84 | }); 85 | newOrder.save((err, saveditem) => { 86 | if (err) return console.log(err); 87 | res.end(); 88 | }) 89 | }) 90 | 91 | router.get('/orders', (req, res)=> { 92 | ModelOrders.find((err, x) => { 93 | err ? res.status(500).send(err) : 94 | res.json(x) 95 | }) 96 | }) 97 | 98 | 99 | router.delete('/delete/item/:id', (req, res, next)=> { 100 | const { id } = req.params 101 | ModelProducts.findByIdAndRemove(id, (err, model) => { 102 | if (err) return handleError(err); 103 | res.send('item: '+id+' deleted'); 104 | }) 105 | .then(()=>{ 106 | const logDelete = new ModelLog({ 107 | type: 'Delete', 108 | time: new Date(), 109 | itemid: id 110 | }); 111 | logDelete.save(function(err, logSaved){ 112 | if(err){return next(err);} 113 | res.end(); 114 | }); 115 | }).catch(err => next(err)); 116 | }); 117 | 118 | router.get('/log', (req, res)=> { 119 | ModelLog.find((err, x) => { 120 | err ? res.status(500).send(err) : 121 | res.json(x) 122 | }) 123 | }) 124 | 125 | 126 | router.get('/secret', passport.authenticate('jwt', {session: false}), (req, res)=> { 127 | res.json({authorization: true}); 128 | }); 129 | 130 | router.post('/signin', passport.authenticate('local', {session:false}), Authentication.signin); 131 | 132 | router.post('/signup', Authentication.signup); 133 | 134 | 135 | module.exports = router; 136 | -------------------------------------------------------------------------------- /client/src/components/Cart.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react' 3 | import { Table, Container, Button } from 'reactstrap'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | const propTypes = { 7 | getCart: PropTypes.array.isRequired, 8 | addToCart: PropTypes.func.isRequired, 9 | deleteFromCart: PropTypes.func.isRequired, 10 | deleteALlFromCart: PropTypes.func.isRequired, 11 | }; 12 | 13 | const styles = { 14 | centerh1: { 15 | textAlign: 'center', 16 | padding: '30px' 17 | }, 18 | images: { 19 | width: '30px' 20 | }, 21 | checkoutBtn: { 22 | textAlign: 'right' 23 | }, 24 | btnIncrement: { 25 | color: '#072a48', 26 | backgroundColor: 'white', 27 | border: 'solid', 28 | borderColor: '#072a48', 29 | width: '30px', 30 | cursor: 'pointer', 31 | borderWidth: '0.1ex' 32 | }, 33 | btnDelete: { 34 | color: 'white', 35 | backgroundColor: '#072a48', 36 | border: 'solid', 37 | borderColor: '#072a48', 38 | width: '30px', 39 | cursor: 'pointer', 40 | borderWidth: '0.1ex' 41 | }, 42 | containerPadding: { 43 | paddingTop: '70px', 44 | paddingBottom: '150px' 45 | } 46 | } 47 | 48 | const Cart = ({ 49 | getCart, 50 | addToCart, 51 | deleteFromCart, 52 | deleteALlFromCart 53 | }) => { 54 | 55 | 56 | const { centerh1, images, checkoutBtn, btnIncrement, btnDelete, containerPadding } = styles 57 | const reducePrice = (getCart.reduce((acc, x) => (acc + (x.quantity * x.price)), 0)) 58 | 59 | return ( 60 |
    61 | 62 |

    Cart

    63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | { 74 | getCart.sort((a, b) => a._id === b._id ? a.selectedSize === b.selectedSize ? a.selectedColor < b.selectedColor : a.selectedSize < b.selectedSize : a._id < b._id).map(x => 75 | 76 | 80 | 83 | 86 | 89 | 90 | ) 91 | } 92 | 93 | 94 | 97 | 100 | 101 | 102 | 103 | 106 | 109 | 110 | 111 | 112 | 115 | 118 | 119 | 120 |
    ItemPriceQuantity
    77 | 78 |

    {x.title} | {x.selectedSize} | {x.selectedColor}

    79 |
    81 |

    {x.price}$

    82 |
    84 |

    {' '+x.quantity+' '}

    85 |
    87 |

    88 |
    95 | Subtotal 96 | 98 | {reducePrice} $ 99 |
    104 | Shipping 105 | 107 | 3 $ 108 |
    113 | Total 114 | 116 | {reducePrice + 3} $ 117 |
    121 |
    122 | 123 |
    124 |
    125 |
    126 | ) 127 | } 128 | 129 | Cart.propTypes = propTypes; 130 | 131 | export default Cart; 132 | -------------------------------------------------------------------------------- /client/src/components/Admin-history-log.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Table, Row, Col } from 'reactstrap'; 3 | import axios from 'axios'; 4 | import {Doughnut,Line} from 'react-chartjs-2'; 5 | 6 | 7 | 8 | export class AdminHistoryLog extends Component { 9 | 10 | constructor(props){ 11 | super(props); 12 | this.state = { 13 | logData: [] 14 | } 15 | } 16 | 17 | componentDidMount = async () => { 18 | try { 19 | const response = await axios.get('/api/log') 20 | const logData = await response.data; 21 | this.setState({ logData }) 22 | } catch (error) { 23 | console.log(error); 24 | } 25 | } 26 | 27 | render() { 28 | const { stylesTab4 } = this.props 29 | const dataDoughnut = { 30 | labels: [ 31 | 'Delete', 32 | 'Create', 33 | 'Update' 34 | ], 35 | datasets: [{ 36 | data: [ 37 | this.state.logData.filter(x=>x.type === 'Delete').length, 38 | this.state.logData.filter(x=>x.type === 'Create').length, 39 | this.state.logData.filter(x=>x.type === 'Update').length, 40 | ], 41 | backgroundColor: [ 42 | '#FF6384', 43 | '#072a48', 44 | '#FFCE56' 45 | ], 46 | hoverBackgroundColor: [ 47 | '#FF6384', 48 | '#36A2EB', 49 | '#FFCE56' 50 | ] 51 | }] 52 | }; 53 | 54 | const dataLine = { 55 | labels: this.state.logData.map(x=> 56 | new Date(x.time).toLocaleDateString('en-TH', { 57 | year: 'numeric', 58 | month: 'long', 59 | day: 'numeric', 60 | hour: '2-digit' 61 | } 62 | ) 63 | ), 64 | datasets: [ 65 | { 66 | label: 'Dashboard activity', 67 | fill: false, 68 | lineTension: 0.1, 69 | backgroundColor: 'rgba(75,192,192,0.4)', 70 | borderColor: 'rgba(75,192,192,1)', 71 | borderCapStyle: 'butt', 72 | borderDash: [], 73 | borderDashOffset: 0.0, 74 | borderJoinStyle: 'miter', 75 | pointBorderColor: 'rgba(75,192,192,1)', 76 | pointBackgroundColor: '#fff', 77 | pointBorderWidth: 1, 78 | pointHoverRadius: 5, 79 | pointHoverBackgroundColor: 'rgba(75,192,192,1)', 80 | pointHoverBorderColor: 'rgba(220,220,220,1)', 81 | pointHoverBorderWidth: 2, 82 | pointRadius: 1, 83 | pointHitRadius: 10, 84 | data: this.state.logData.map((x,index)=>index) 85 | } 86 | ] 87 | }; 88 | 89 | const rows = 90 | this.state.logData.map((x, index)=> 91 | 92 | {index+1} 93 | {x.itemid} 94 | {x.itemtitle} 95 | {x.type} 96 | { new Date(x.time).toLocaleDateString('en-TH', { 97 | year: 'numeric', 98 | month: 'long', 99 | day: 'numeric', 100 | hour: '2-digit' 101 | } 102 | )} 103 | 104 | ) 105 | return ( 106 |
    107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | {rows} 121 | 122 |
    #Product IDProduct titleTypeTime stamp
    123 | 124 | 125 | 126 |
    127 | 128 |
    129 |
    130 | 131 |
    132 | 133 |
    134 | 135 | 136 |
    137 | 138 | ) 139 | } 140 | } 141 | 142 | export default AdminHistoryLog 143 | -------------------------------------------------------------------------------- /client/src/components/Breadcrumbs.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Fragment } from 'react'; 3 | import { Container, Row, Col } from 'reactstrap'; 4 | import { isMobile } from "react-device-detect"; 5 | import { Link } from 'react-router-dom'; 6 | import ButtonSorter from './Button-sorter'; 7 | import ButtonFilterMobile from './Button-filter-mobile'; 8 | 9 | 10 | const propTypes = { 11 | gender: PropTypes.string, 12 | selectedCategory: PropTypes.array, 13 | backgroundColor: PropTypes.string, 14 | textColor: PropTypes.string, 15 | marginTop: PropTypes.number, 16 | showSortBtn: PropTypes.bool, 17 | showFilterBtn: PropTypes.bool, 18 | dispatchToSortList: PropTypes.func, 19 | sortArgsForFilter: PropTypes.string, 20 | dispatchSize: PropTypes.func, 21 | sortSizeForFilter: PropTypes.string, 22 | keywordsSelectAction: PropTypes.func, 23 | categoriesProducts: PropTypes.object, 24 | keywordsForFilter: PropTypes.array, 25 | actionPriceRangeFilter: PropTypes.func, 26 | reducerPriceRangeFilter: PropTypes.number, 27 | listLength: PropTypes.number, 28 | }; 29 | 30 | const Breadcrumbs = ({ 31 | gender, 32 | selectedCategory, 33 | backgroundColor, 34 | textColor, 35 | marginTop, 36 | showSortBtn, 37 | showFilterBtn, 38 | dispatchToSortList, 39 | sortArgsForFilter, 40 | dispatchSize, 41 | sortSizeForFilter, 42 | keywordsSelectAction, 43 | categoriesProducts, 44 | keywordsForFilter, 45 | actionPriceRangeFilter, 46 | reducerPriceRangeFilter, 47 | listLength 48 | }) => { 49 | 50 | const styles= { 51 | containerPcScreen: { 52 | height: '80px', 53 | backgroundColor: backgroundColor, 54 | marginTop: marginTop, 55 | color: textColor, 56 | display: 'flex', 57 | justifyContent: 'left', 58 | alignItems: 'center' 59 | }, 60 | containerMobileScreen: { 61 | height: '100px', 62 | backgroundColor: backgroundColor, 63 | marginTop: marginTop, 64 | color: textColor, 65 | display: 'flex', 66 | justifyContent: 'left', 67 | alignItems: 'center' 68 | }, 69 | sortBtnMobileScreen: { 70 | display: 'flex', 71 | justifyContent: 'center', 72 | alignItems: 'center' 73 | }, 74 | linkColor: { 75 | color: textColor 76 | } 77 | } 78 | 79 | const sortBtn = 80 | showSortBtn && 81 | 82 | {showFilterBtn && 83 | 95 | } 96 | 100 | 101 | 102 | const genderLink = gender && {` ${gender && gender.charAt(0).toUpperCase() + gender.substr(1)}'s Apparels`} >; 103 | const selectedItem = selectedCategory.length === 1 ? ' '+selectedCategory : selectedCategory.length > 1 ? ' Multiple criterias' : ' Category selection' 104 | return ( 105 |
    106 | 107 | 108 | 109 |
    110 | Home > 111 | {genderLink} 112 | {selectedItem} 113 |
    114 | 115 | { sortBtn } 116 |
    117 |
    118 |
    119 | ); 120 | } 121 | 122 | Breadcrumbs.propTypes = propTypes; 123 | 124 | export default Breadcrumbs; 125 | -------------------------------------------------------------------------------- /client/src/components/Carousel-homepage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Carousel, 4 | CarouselItem, 5 | CarouselControl, 6 | Col, 7 | Row, 8 | } from 'reactstrap'; 9 | import ButtonInternalLink from './Button-internal-link'; 10 | 11 | const styles = { 12 | sliderContent: { 13 | display: 'flex', 14 | alignItems: 'center', 15 | justifyContent: 'center', 16 | flexDirection: 'column', 17 | color:'white' 18 | } 19 | } 20 | 21 | const items = [ 22 | { 23 | src: 'https://images.unsplash.com/photo-1489987707025-afc232f7ea0f?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=b17958c40e777065643cb65954bede79&dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb', 24 | altText: 'Slide 1', 25 | caption: 'Slide 1', 26 | title: 'Fashion is cool', 27 | subtitle: 'Check our nice collection', 28 | btn: { 29 | content: 'Go to the collection', 30 | link: '/category/men' 31 | 32 | } 33 | }, 34 | { 35 | src: 'https://images.unsplash.com/photo-1488826701985-4c18de62b405?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=9ce707ffe1b3f8c031acc5788cf6aef2&dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb', 36 | altText: 'Slide 2', 37 | caption: 'Slide 2', 38 | title: "Nice for couples", 39 | subtitle: 'Collection summer 2018', 40 | btn: { 41 | content: 'Cool collection', 42 | link: '/category/men' 43 | 44 | } 45 | }, 46 | { 47 | src: 'https://images.unsplash.com/photo-1485331129317-1717811a2b75?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=1b713c86ebb20befc80029db6bc98dae&dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb', 48 | altText: 'Slide 3', 49 | caption: 'Slide 3', 50 | title: 'Nice for everyone', 51 | subtitle: 'Amazing clothes!', 52 | btn: { 53 | content: 'Go to the collection', 54 | link: '/category/men' 55 | 56 | } 57 | } 58 | ]; 59 | 60 | class CarouselHomepage extends Component { 61 | constructor(props) { 62 | super(props); 63 | this.state = { activeIndex: 0 }; 64 | } 65 | 66 | onExiting = () => { 67 | this.animating = true; 68 | } 69 | 70 | onExited = () => { 71 | this.animating = false; 72 | } 73 | 74 | next = () => { 75 | if (this.animating) return; 76 | const nextIndex = this.state.activeIndex === items.length - 1 ? 0 : this.state.activeIndex + 1; 77 | this.setState({ activeIndex: nextIndex }); 78 | } 79 | 80 | previous = () => { 81 | if (this.animating) return; 82 | const nextIndex = this.state.activeIndex === 0 ? items.length - 1 : this.state.activeIndex - 1; 83 | this.setState({ activeIndex: nextIndex }); 84 | } 85 | 86 | goToIndex = (newIndex) => { 87 | if (this.animating) return; 88 | this.setState({ activeIndex: newIndex }); 89 | } 90 | 91 | render() { 92 | const { activeIndex } = this.state; 93 | 94 | const slides = items.map(x => { 95 | return ( 96 | 101 | 102 | 103 | {x.altText} 104 | 105 | 106 |

    {x.title}

    107 |

    {x.subtitle}

    108 | 114 | 115 |
    116 |
    117 | ); 118 | }); 119 | 120 | return ( 121 | 126 | {slides} 127 | 128 | 129 | 130 | ); 131 | } 132 | } 133 | 134 | 135 | export default CarouselHomepage; 136 | -------------------------------------------------------------------------------- /client/src/components/Carousel-item.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Carousel, 4 | CarouselItem, 5 | CarouselControl, 6 | CarouselIndicators 7 | } from 'reactstrap'; 8 | 9 | const items = [ 10 | { 11 | src: 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa1d%20text%20%7B%20fill%3A%23555%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa1d%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23777%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22285.921875%22%20y%3D%22218.3%22%3EFirst%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E', 12 | altText: 'Slide 1', 13 | caption: 'Slide 1' 14 | }, 15 | { 16 | src: 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa20%20text%20%7B%20fill%3A%23444%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa20%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23666%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22247.3203125%22%20y%3D%22218.3%22%3ESecond%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E', 17 | altText: 'Slide 2', 18 | caption: 'Slide 2' 19 | }, 20 | { 21 | src: 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa21%20text%20%7B%20fill%3A%23333%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa21%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23555%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22277%22%20y%3D%22218.3%22%3EThird%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E', 22 | altText: 'Slide 3', 23 | caption: 'Slide 3' 24 | } 25 | ]; 26 | 27 | class CarouselItemPage extends Component { 28 | constructor(props) { 29 | super(props); 30 | this.state = { activeIndex: 0 }; 31 | } 32 | 33 | onExiting = () => { 34 | this.animating = true; 35 | } 36 | 37 | onExited = () => { 38 | this.animating = false; 39 | } 40 | 41 | next = () => { 42 | if (this.animating) return; 43 | const nextIndex = this.state.activeIndex === this.props.imgsArray.length - 1 ? 0 : this.state.activeIndex + 1; 44 | this.setState({ activeIndex: nextIndex }); 45 | } 46 | 47 | previous = () => { 48 | if (this.animating) return; 49 | const nextIndex = this.state.activeIndex === 0 ? this.props.imgsArray.length - 1 : this.state.activeIndex - 1; 50 | this.setState({ activeIndex: nextIndex }); 51 | } 52 | 53 | goToIndex = (newIndex) => { 54 | if (this.animating) return; 55 | this.setState({ activeIndex: newIndex }); 56 | } 57 | 58 | render() { 59 | const { activeIndex } = this.state; 60 | 61 | const slides = this.props.imgsArray.map((x, index) => { 62 | return ( 63 | 68 | {x} 69 | 70 | ); 71 | }); 72 | 73 | return ( 74 | 79 | 80 | {slides} 81 | 82 | 83 | 84 | ); 85 | } 86 | } 87 | 88 | 89 | export default CarouselItemPage; 90 | -------------------------------------------------------------------------------- /client/src/components/Items-list-gender-homepage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import { 5 | oneKeywordForFilter 6 | } from '../actions/DataFetchingActions'; 7 | import { 8 | Row, 9 | Col, 10 | Button, 11 | Card, 12 | CardImg, 13 | CardBody, 14 | Alert, 15 | Container 16 | } from 'reactstrap'; 17 | 18 | const styles = { 19 | cardTitle: { 20 | display: 'flex', 21 | justifyContent: 'center', 22 | fontSize: '20px' 23 | }, 24 | cardBtn: { 25 | textAlign:'center' 26 | }, 27 | card: { 28 | marginBottom: '20px' 29 | } 30 | }; 31 | 32 | const categoriesMenData = [ 33 | { 34 | imgSrc:'https://images.unsplash.com/photo-1490367532201-b9bc1dc483f6?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=717ae13fa0e515d1c7c9e92456439845&dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb', 35 | cardTitle:'Polos', 36 | linkCategory:'Polos' 37 | }, 38 | { 39 | imgSrc:'https://images.unsplash.com/photo-1530856021941-02c71be5926f?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=8d29673aa02db77423cf5ca770bd6d8e&dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb', 40 | cardTitle:'Shirts', 41 | linkCategory:'Shirts' 42 | }, 43 | { 44 | imgSrc:'https://images.unsplash.com/photo-1421986386270-978ed214ec60?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=0f6cec57034960f50caf64c5af9c26d4&dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb', 45 | cardTitle:'Pants', 46 | linkCategory:'Pants' 47 | }, 48 | { 49 | imgSrc:'https://images.unsplash.com/photo-1530856021941-02c71be5926f?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=8d29673aa02db77423cf5ca770bd6d8e&dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb', 50 | cardTitle:'Jackets', 51 | linkCategory:'Jackets' 52 | } 53 | ]; 54 | 55 | const categoriesWomenData = [ 56 | { 57 | imgSrc:'https://images.unsplash.com/photo-1472746729193-36ad213ac4a5?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=27aecaf25f31cf45d2de3ad774dad6ed&dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb', 58 | cardTitle:'Dresses', 59 | linkCategory:'Dresses' 60 | }, 61 | { 62 | imgSrc:'https://images.unsplash.com/photo-1485527691629-8e370684924c?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=d6f450a6506599df62dc29593779a1b3&dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb', 63 | cardTitle:'Cardigans', 64 | linkCategory:'Cardigans' 65 | }, 66 | { 67 | imgSrc:'https://images.unsplash.com/photo-1518622358385-8ea7d0794bf6?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=42226a7bf3b99eec89267859b4f36114&dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb', 68 | cardTitle:'Tops', 69 | linkCategory:'Tops' 70 | }, 71 | { 72 | imgSrc:'https://images.unsplash.com/photo-1508445861827-7711f397113a?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=9b8561c6e78192892aae3c6943928c93&dpr=1&auto=format&fit=crop&w=1000&q=80&cs=tinysrgb', 73 | cardTitle:'Trench Coats', 74 | linkCategory:'Trench Coats' 75 | } 76 | ]; 77 | 78 | const decorationData = gender => gender === 'women' ? categoriesWomenData : categoriesMenData; 79 | 80 | const eachCategory = (gender, oneKeywordForFilter) => ( 81 | decorationData(gender).map(x=>( 82 | 83 | 84 |
    oneKeywordForFilter(x.cardTitle)}> 85 | 86 | 87 |
    88 | {x.cardTitle} 89 |
    90 | 91 |
    92 | 93 |
    94 | 95 | )) 96 | ); 97 | 98 | 99 | const ItemsListGenderHomepage = props => { 100 | const { gender } = props.match.params 101 | const { oneKeywordForFilter } = props 102 | return ( 103 | 104 | 105 |

    {gender.charAt(0).toUpperCase()+gender.slice(1)} selection

    106 | 107 | {eachCategory(gender, oneKeywordForFilter)} 108 | 109 | 110 |
    111 | 112 | )}; 113 | 114 | const mapDispatchToProps = dispatch => ({oneKeywordForFilter: x => dispatch(oneKeywordForFilter(x))}); 115 | const mapStateToProps = state => ({oneKeywordForFilter: state.oneKeywordForFilter}); 116 | 117 | export default connect(mapStateToProps, mapDispatchToProps)(ItemsListGenderHomepage); 118 | -------------------------------------------------------------------------------- /client/src/containers/Checkout-container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import axios from 'axios'; 3 | import { connect } from 'react-redux'; 4 | import { selectorTotalAmountCart } from '../selectors/selector_list_statistics'; 5 | import Checkout from '../components/Checkout'; 6 | import { addUserAddress } from '../actions/UsersActions'; 7 | 8 | class CheckoutContainer extends Component { 9 | constructor(props){ 10 | super(props); 11 | this.state={ 12 | step1: true, 13 | step2: false, 14 | step2Unlock: false, 15 | step3: false, 16 | step3Unlock: false, 17 | email: '', 18 | emailIsValid: true, 19 | firstName: 'fgsfdsf', 20 | lastName: 'fsdfsd', 21 | country: 'USA', 22 | city: 'fdsfdsfdsf', 23 | province: 'fdsfdsf', 24 | postalCode: 8767671167, 25 | phoneNumber: 17671651, 26 | address1: 'qdfsqfdsqfcfdxsf', 27 | address2: 'xsfdsxfsdfxsdf', 28 | shippingMethod: 2, 29 | formIsValid: false, 30 | totalDelivery: 5 31 | } 32 | }; 33 | 34 | onChangeEmail = e => this.setState({ email: e.target.value}); 35 | onChangeFirstName = e => this.setState({ firstName: e.target.value }); 36 | onChangeLastName = e => this.setState({ lastName: e.target.value }); 37 | onChangeCountry = e => this.setState({ country: e.target.value }); 38 | onChangeCity = e => this.setState({ city: e.target.value }); 39 | onChangeProvince = e => this.setState({ province: e.target.value }); 40 | onChangePostalCode = e => this.setState({ postalCode: Number(e.target.value) }); 41 | onChangePhoneNumber = e => this.setState({ phoneNumber: Number(e.target.value) }); 42 | onChangeAdress1 = e => this.setState({ address1: e.target.value }); 43 | onChangeAdress2 = e => this.setState({ address2: e.target.value }); 44 | onChangeShipppingMethod = shippingMethod => this.setState({ shippingMethod }); 45 | handleEmailValidation = emailIsValid => this.setState({ emailIsValid }); 46 | formValidator = formIsValid => this.setState({ formIsValid }); 47 | 48 | toggle = step => { 49 | step === 'step1' ? this.setState({ 50 | step1: true, 51 | step2: false, 52 | step3: false 53 | }) : 54 | step === 'step2' ? this.setState({ 55 | step1: false, 56 | step2: true, 57 | step3: false 58 | }) : 59 | step === 'step3' && this.setState({ 60 | step1: false, 61 | step2: false, 62 | step3: true 63 | }) 64 | }; 65 | 66 | stepsUnlock = step => { 67 | step === 'step2' ? this.setState({ step2Unlock: true, step1: false, step2: true, step3: false }) : 68 | step === 'step3' && this.setState({ step3Unlock: true, step1: false, step2: false, step3: true }) 69 | }; 70 | 71 | onSubmitOrder = () => { 72 | const { email, firstName, lastName, country, city, province, postalCode, phoneNumber, address1, address2, totalDelivery } = this.state; 73 | const { getCart, selectorTotalAmountCart } = this.props; 74 | const ref = Math.random().toString(36).substring(5)+ Date.now(); 75 | axios.post('/api/add/orders', { 76 | ref, 77 | customerinfo: {email, firstName, lastName, country, city, province, postalCode, phoneNumber, address1, address2}, 78 | order: getCart.map(x => ({idItem: x._id, titleItem:x.title, selectedSize:x.selectedSize, selectedColor:x.selectedColor, price:x.price, quantity:x.quantity})), 79 | totalDelivery, 80 | totalAmount: selectorTotalAmountCart 81 | }) 82 | .catch(error => { 83 | console.log(error); 84 | }); 85 | }; 86 | 87 | render() { 88 | return ( 89 |
    90 | 110 |
    111 | 112 | ) 113 | } 114 | }; 115 | 116 | 117 | const mapStateToProps = state => ({ 118 | getCart: state.cartReducer, 119 | getUserAddress: state.getUserAddress, 120 | selectorTotalAmountCart: selectorTotalAmountCart(state) 121 | }); 122 | 123 | const mapDispatchToProps = dispatch => ({ 124 | addUserAddress: x => dispatch(addUserAddress(x)) 125 | }); 126 | 127 | export default connect(mapStateToProps, mapDispatchToProps)(CheckoutContainer); 128 | 129 | -------------------------------------------------------------------------------- /client/src/containers/Navbar-container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link, NavLink } from 'react-router-dom' 3 | import { 4 | isBrowser 5 | } from "react-device-detect"; 6 | import { connect } from 'react-redux' 7 | import { oneKeywordForFilter, resetKeywords} from '../actions/DataFetchingActions'; 8 | import { selectorTotalItemsCart } from '../selectors/selector_list_statistics'; 9 | import CheckoutMiniSummaryPreview from '../components/Checkout-mini-summary-preview'; 10 | import Submenu from '../components/Submenu'; 11 | import { 12 | Collapse, 13 | Navbar, 14 | NavbarToggler, 15 | Nav, 16 | NavItem, 17 | Badge 18 | } from 'reactstrap'; 19 | 20 | const styles = { 21 | itemMenu: { 22 | padding: '10px', 23 | listStyleType: 'none' 24 | }, 25 | arrowDown: { 26 | width: 0, 27 | height: 0, 28 | borderStyle: 'solid', 29 | borderWidth: '30px 18px 0 18px', 30 | borderColor:' #072a48 transparent transparent transparent', 31 | position: 'absolute', 32 | zIndex:'3' 33 | }, 34 | navbarBackground: { 35 | backgroundColor: '#072a48', 36 | zIndex: 3 37 | } 38 | } 39 | 40 | const arrowStyleSubmenu = (subMenuCategorySelected, gender, arrowDown) => subMenuCategorySelected === gender &&
    41 | 42 | class NavbarContainer extends Component { 43 | constructor(props) { 44 | super(props); 45 | 46 | this.toggle = this.toggle.bind(this); 47 | this.state = { 48 | isOpen: false, 49 | subMenuOpen: false, 50 | subMenuCategorySelected: '', 51 | openCartPreview: false 52 | }; 53 | } 54 | toggle() { 55 | this.setState({ 56 | isOpen: !this.state.isOpen 57 | }); 58 | } 59 | 60 | handleSubMenuEnter = (x) => { 61 | this.setState({ 62 | subMenuOpen: true, 63 | subMenuCategorySelected: x 64 | }) 65 | } 66 | 67 | handleSubMenuExit = () => { 68 | this.setState({ 69 | subMenuOpen: false, 70 | subMenuCategorySelected: '' 71 | }) 72 | } 73 | 74 | 75 | 76 | render() { 77 | const { sendOneKeyword, getCart, resetKeywords, totalItemsSelectorStats } = this.props 78 | const { isOpen, subMenuCategorySelected, subMenuOpen, openCartPreview } = this.state 79 | const { men, women } = this.props.categoriesProducts 80 | const { itemMenu, arrowDown, navbarBackground } = styles 81 | 82 | const categoriesNavItems = gender => 83 | isBrowser ? 84 | (this.handleSubMenuEnter(gender)} > 85 | resetKeywords()}>{gender.charAt(0).toUpperCase() + gender.slice(1)} {arrowStyleSubmenu(subMenuCategorySelected, gender, arrowDown)} 86 | ) : 87 | ( 88 | {return (resetKeywords(), this.toggle())}}>{gender} 89 | ) 90 | 91 | const cartNavItem = 92 | isBrowser ? 93 | () : cart 107 | 108 | const subMenuHoverBrowser = 109 | subMenuOpen && isBrowser && 110 | 116 | 117 | 118 | return ( 119 |
    120 | 121 | Home 122 | 123 | 124 | {categoriesNavItems('men')} 125 | {categoriesNavItems('women')} 126 | {cartNavItem} 127 | 128 | 129 | {subMenuHoverBrowser} 130 |
    131 | ); 132 | } 133 | }; 134 | 135 | 136 | const mapStateToProps = state => ({ 137 | categoriesProducts: state.categoriesProducts, 138 | getCart: state.cartReducer, 139 | totalItemsSelectorStats: selectorTotalItemsCart(state) 140 | }); 141 | 142 | const mapDispatchToProps = dispatch => ({ 143 | sendOneKeyword: x => dispatch(oneKeywordForFilter(x)), 144 | resetKeywords: () => dispatch(resetKeywords()) 145 | }); 146 | 147 | 148 | export default connect(mapStateToProps, mapDispatchToProps)(NavbarContainer); 149 | -------------------------------------------------------------------------------- /client/src/components/Admin-modal-update.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Modal, ModalHeader, ModalBody, ModalFooter, ListGroup, InputGroup, InputGroupAddon, InputGroupText, Input } from 'reactstrap'; 3 | import { FiEdit } from 'react-icons/fi'; 4 | import axios from 'axios'; 5 | 6 | class AdminModalUpdate extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | modalEdit: false, 11 | title: this.props.infos.title, 12 | colors: this.props.infos.color, 13 | sizes: this.props.infos.size, 14 | tags: this.props.infos.tags, 15 | images: this.props.infos.images, 16 | description: this.props.infos.description, 17 | price: this.props.infos.price 18 | }; 19 | } 20 | 21 | toggle = () => this.setState({ modalEdit: !this.state.modalEdit }); 22 | 23 | onSubmit = (id, title, price, color, size, tags, images, description) => { 24 | axios.post('/api/update/item', { 25 | id, 26 | title, 27 | price, 28 | color: (color.slice(0)+'').replace(/\s/g,'').split(','), 29 | size: (size.slice(0)+'').replace(/\s/g,'').split(','), 30 | tags: (tags.slice(0)+'').replace(/\s/g,'').split(','), 31 | images: (images.slice(0)+'').replace(/\s/g,'').split(','), 32 | description 33 | }) 34 | .then(response => { 35 | this.setState({ modalEdit: !this.state.modalEdit }); 36 | console.log(response); 37 | }) 38 | .then(() => { 39 | window.location.reload(true) 40 | }) 41 | .catch(err => { 42 | console.log(err); 43 | }); 44 | } 45 | 46 | onChangeTitle = (e) => this.setState({title: e.target.value}) 47 | onChangePrice = (e) => this.setState({price: e.target.value}) 48 | onChangeColor = (e) => this.setState({colors: e.target.value}) 49 | onChangeSizes = (e) => this.setState({sizes: e.target.value}) 50 | onChangeTags = (e) => this.setState({tags: e.target.value}) 51 | onChangeImages = (e) => this.setState({images: e.target.value}) 52 | onChangeDescription = (e) => this.setState({description: e.target.value}) 53 | 54 | 55 | render() { 56 | const { title, colors, sizes, tags, images, description, price } = this.state 57 | const { _id } = this.props.infos 58 | 59 | return ( 60 |
    61 | 62 | 63 | {this.props.infos.title} - (id: {_id}) 64 | 65 | 66 | 67 | 68 | Name 69 | 70 | 71 | 72 | 73 | 74 | Price $ 75 | 76 | 77 | 78 | 79 | 80 | Colors 81 | 82 | ' '+x)} value={colors} onChange={this.onChangeColor} /> 83 | 84 | 85 | 86 | Sizes 87 | 88 | x+' '+x)} value={sizes} onChange={this.onChangeSizes} /> 89 | 90 | 91 | 92 | Category 93 | 94 | 95 | 96 | 97 | 98 | Images 99 | 100 | x+' '+x)} value={images} onChange={this.onChangeImages} /> 101 | 102 | 103 | 104 | description 105 | 106 | 107 | 108 | 109 | 110 | 111 | {' '} 123 | 124 | 125 | 126 |
    127 | ); 128 | } 129 | }; 130 | 131 | export default AdminModalUpdate; 132 | -------------------------------------------------------------------------------- /client/src/components/Items-list.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { isBrowser, isMobile } from "react-device-detect"; 4 | import ItemsListBanner from './Items-list-banner'; 5 | import Breadcrumbs from './Breadcrumbs'; 6 | import ItemsListSidebar from './Items-list-sidebar'; 7 | import Paginator from './Paginator'; 8 | import EachItemInList from './Each-item-in-list'; 9 | import LoadingGif from './Loading-gif'; 10 | import ButtonLinkGenderPage from './Button-link-gender-page' 11 | import { 12 | Container, 13 | Row, 14 | Col 15 | } from 'reactstrap'; 16 | 17 | const propTypes = { 18 | listIsLoading: PropTypes.bool.isRequired, 19 | FilteredSortedList: PropTypes.array.isRequired, 20 | keywordsForFilter: PropTypes.array.isRequired, 21 | oneKeywordForFilter: PropTypes.func.isRequired, 22 | currentPageHandler: PropTypes.func.isRequired, 23 | currentPage: PropTypes.number.isRequired, 24 | itemsMaxPage: PropTypes.number.isRequired, 25 | dispatchSize: PropTypes.func.isRequired, 26 | sortSizeForFilter: PropTypes.string.isRequired, 27 | sortArgsForFilter: PropTypes.string.isRequired, 28 | dispatchToSortList: PropTypes.func.isRequired, 29 | keywordsSelectAction: PropTypes.func.isRequired, 30 | categoriesProducts: PropTypes.object.isRequired, 31 | actionPriceRangeFilter: PropTypes.func.isRequired, 32 | reducerPriceRangeFilter: PropTypes.number.isRequired, 33 | actionFillFilters: PropTypes.func.isRequired 34 | } 35 | 36 | const styles = { 37 | spaceColumn: { 38 | marginLeft: '25px', 39 | marginRight: '25px', 40 | marginBottom: '50px' 41 | }, 42 | fontSize: { 43 | fontSize: '15px' 44 | }, 45 | marginLeftBtn: { 46 | marginLeft: '30px' 47 | }, 48 | containerPaddingTop: { 49 | paddingTop: '35px' 50 | } 51 | }; 52 | 53 | const ItemsList = ({ 54 | match, 55 | listIsLoading, 56 | FilteredSortedList, 57 | keywordsForFilter, 58 | oneKeywordForFilter, 59 | currentPageHandler, 60 | currentPage, 61 | itemsMaxPage, 62 | dispatchSize, 63 | sortSizeForFilter, 64 | sortArgsForFilter, 65 | dispatchToSortList, 66 | keywordsSelectAction, 67 | categoriesProducts, 68 | actionPriceRangeFilter, 69 | reducerPriceRangeFilter, 70 | actionFillFilters 71 | }) => { 72 | 73 | const { gender } = match.params; 74 | const listLength = FilteredSortedList.length 75 | 76 | const loading_logic = listIsLoading && ; 77 | 78 | const pagination = Math.ceil(listLength/itemsMaxPage)>1 ? 79 | () : 85 | currentPage > 1 && (()=> currentPageHandler('empty'))() 86 | 87 | 88 | 89 | const itemsListByGender_logic = 90 | 91 | 92 | {listIsLoading === false && Results: {listLength}} 93 | {FilteredSortedList.length === 0 && listIsLoading === false && 94 |
    95 |

    Select a category:

    96 | 97 | 98 |
    } 99 | 100 | 101 | {loading_logic} {/* if list is loading show loader */} 102 | 109 | 110 | {pagination} 111 | ; 112 | 113 | const sideBar = isBrowser && 114 | 115 | 116 | 128 | 129 | ; 130 | 131 | 132 | return ( 133 |
    134 | 141 | 0} 151 | showFilterBtn={isMobile} 152 | dispatchSize={dispatchSize} 153 | sortSizeForFilter={sortSizeForFilter} 154 | keywordsSelectAction={keywordsSelectAction} 155 | categoriesProducts={categoriesProducts} 156 | actionPriceRangeFilter={actionPriceRangeFilter} 157 | reducerPriceRangeFilter={reducerPriceRangeFilter} 158 | listLength={listLength} 159 | /> 160 | 161 | 162 | {itemsListByGender_logic} {/* show list depending on gender */} 163 | {sideBar} 164 | 165 | 166 |
    167 | ); 168 | }; 169 | 170 | ItemsList.propTypes = propTypes; 171 | 172 | export default ItemsList; 173 | -------------------------------------------------------------------------------- /client/src/components/Checkout.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import CheckoutStepOne from './Checkout-step-one'; 4 | import CheckoutStepTwo from './Checkout-step-two'; 5 | import CheckoutStepThree from './Checkout-step-three'; 6 | import CheckoutMiniSummary from '../components/Checkout-mini-summary'; 7 | import { 8 | Row, 9 | Col, 10 | Container, 11 | Progress, 12 | ListGroup 13 | } from 'reactstrap'; 14 | 15 | const propTypes = { 16 | getCart: PropTypes.array.isRequired, 17 | addUserAddress: PropTypes.func.isRequired, 18 | getUserAddress: PropTypes.object.isRequired, 19 | step1: PropTypes.bool.isRequired, 20 | step2: PropTypes.bool.isRequired, 21 | step3: PropTypes.bool.isRequired, 22 | step2Unlock: PropTypes.bool.isRequired, 23 | step3Unlock: PropTypes.bool.isRequired, 24 | email: PropTypes.string.isRequired, 25 | firstName: PropTypes.string.isRequired, 26 | lastName: PropTypes.string.isRequired, 27 | country: PropTypes.string.isRequired, 28 | city: PropTypes.string.isRequired, 29 | province: PropTypes.string.isRequired, 30 | postalCode: PropTypes.number.isRequired, 31 | phoneNumber: PropTypes.number.isRequired, 32 | address1: PropTypes.string.isRequired, 33 | address2: PropTypes.string.isRequired, 34 | shippingMethod: PropTypes.number.isRequired, 35 | formIsValid: PropTypes.bool.isRequired, 36 | onChangeFirstName: PropTypes.func.isRequired, 37 | onChangeLastName: PropTypes.func.isRequired, 38 | onChangeCountry: PropTypes.func.isRequired, 39 | onChangeCity: PropTypes.func.isRequired, 40 | onChangeProvince: PropTypes.func.isRequired, 41 | onChangePostalCode: PropTypes.func.isRequired, 42 | onChangePhoneNumber: PropTypes.func.isRequired, 43 | onChangeAdress1: PropTypes.func.isRequired, 44 | onChangeAdress2: PropTypes.func.isRequired, 45 | onChangeShipppingMethod: PropTypes.func.isRequired, 46 | onChangeEmail: PropTypes.func.isRequired, 47 | toggle: PropTypes.func.isRequired, 48 | stepsUnlock: PropTypes.func.isRequired, 49 | formValidator: PropTypes.func.isRequired, 50 | onSubmitOrder: PropTypes.func.isRequired 51 | }; 52 | 53 | const styles = { 54 | container: { 55 | paddingTop: '120px', 56 | paddingBottom: '120px' 57 | }, 58 | h1: { 59 | paddingBottom: '20px' 60 | }, 61 | collapsePannel: { 62 | borderBottom: '1px solid grey' 63 | }, 64 | collapasePannelTitle: { 65 | cursor: 'pointer' 66 | }, 67 | formFieldsSpace: { 68 | paddingTop: '10px' 69 | } 70 | } 71 | 72 | const CheckoutContainer = ({ 73 | getCart, 74 | addUserAddress, 75 | getUserAddress, 76 | step1, 77 | step2, 78 | step3, 79 | step2Unlock, 80 | step3Unlock, 81 | email, 82 | firstName, 83 | lastName, 84 | country, 85 | city, 86 | province, 87 | postalCode, 88 | phoneNumber, 89 | address1, 90 | address2, 91 | shippingMethod, 92 | formIsValid, 93 | onChangeFirstName, 94 | onChangeLastName, 95 | onChangeCountry, 96 | onChangeCity, 97 | onChangeProvince, 98 | onChangePostalCode, 99 | onChangePhoneNumber, 100 | onChangeAdress1, 101 | onChangeAdress2, 102 | onChangeShipppingMethod, 103 | onChangeEmail, 104 | toggle, 105 | stepsUnlock, 106 | formValidator, 107 | emailIsValid, 108 | handleEmailValidation, 109 | onSubmitOrder, 110 | selectorTotalAmountCart, 111 | totalDelivery 112 | }) => ( 113 |
    114 | 115 | 116 |

    Check out

    117 | 118 | 119 | 120 | 132 | 163 | 173 | 174 | 175 | 176 | 181 | 182 | 183 |
    184 |
    185 | ); 186 | 187 | CheckoutContainer.propTypes = propTypes; 188 | 189 | export default CheckoutContainer; 190 | -------------------------------------------------------------------------------- /client/src/components/Item.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React, { Component } from "react"; 3 | import { isBrowser } from "react-device-detect"; 4 | import Breadcrumbs from "./Breadcrumbs"; 5 | import { Col, Row, Container } from "reactstrap"; 6 | import StarRatings from "react-star-ratings"; 7 | import ButtonSizeSelect from "./Button-size-select"; 8 | import ButtonAddToCart from "./Button-add-to-cart"; 9 | import CheckoutModal from "./Checkout-modal"; 10 | import ButtonsColorSelect from "./Buttons-color-select"; 11 | import CarouselItemPage from "./Carousel-item"; 12 | 13 | const propTypes = { 14 | infoItem: PropTypes.array.isRequired, 15 | addToCart: PropTypes.func.isRequired, 16 | selectedSize: PropTypes.string.isRequired, 17 | selectedColor: PropTypes.string.isRequired, 18 | loading: PropTypes.bool.isRequired, 19 | errorFetching: PropTypes.bool, 20 | handleSizeSelection: PropTypes.func.isRequired, 21 | handleColorSelection: PropTypes.func.isRequired, 22 | totalItemsSelectorStats: PropTypes.number.isRequired 23 | }; 24 | 25 | const styles = { 26 | marginTop: "20px" 27 | }; 28 | 29 | class Item extends Component { 30 | constructor(props) { 31 | super(props); 32 | this.state = { 33 | selectedImage: 0, 34 | openModal: false 35 | }; 36 | } 37 | 38 | toggleModal = () => this.setState({ openModal: !this.state.openModal }); 39 | 40 | render() { 41 | const { 42 | infoItem, 43 | loading, 44 | errorFetching, 45 | addToCart, 46 | handleSizeSelection, 47 | handleColorSelection, 48 | validateSizeSelection, 49 | validateColorSelection, 50 | selectedSize, 51 | selectedColor, 52 | totalItemsSelectorStats, 53 | colorSelectionMissingRemark, 54 | sizeSelectionMissingRemark 55 | } = this.props; 56 | 57 | const { selectedImage } = this.state; 58 | 59 | if (errorFetching) { 60 | return

    Sorry! There was an error loading the items

    ; 61 | } 62 | 63 | const { size, images, color, tags } = infoItem; 64 | if ( 65 | loading || 66 | size === undefined || 67 | images === undefined || 68 | color === undefined || 69 | tags === undefined 70 | ) { 71 | return
    ; 72 | } 73 | const thumbnailsBrowersView = infoItem.images.map((x, index) => ( 74 |
    75 | this.setState({ selectedImage: index })} 77 | src={infoItem.images[index]} 78 | alt="Smiley face" 79 | width="50px" 80 | height="70px" 81 | style={{cursor: 'pointer'}} 82 | /> 83 |
    84 | )); 85 | const MainImageBrowserView = ( 86 | Smiley face 91 | ); 92 | const MainImageMobileView = ( 93 | 94 | ); 95 | 96 | return ( 97 |
    98 | 103 | 104 | 105 | {isBrowser && thumbnailsBrowersView} 106 | 107 | {isBrowser ? MainImageBrowserView : MainImageMobileView} 108 | 109 | 110 |

    {infoItem.title}

    111 |
    {infoItem.price} $
    112 | 121 |
    122 | 130 | {sizeSelectionMissingRemark.length > 0 ? ( 131 | *{sizeSelectionMissingRemark} 132 | ) : ( 133 | "" 134 | )} 135 |
    136 |
    137 |

    138 | Selected color:{" "} 139 | {selectedColor === "" ? Choose below : selectedColor} 140 |

    141 | 147 | {colorSelectionMissingRemark.length > 0 ? ( 148 | *{colorSelectionMissingRemark} 149 | ) : ( 150 | "" 151 | )} 152 |
    153 |
    154 | 166 |
    167 | 175 |

    Description:

    176 |

    {infoItem.description}

    177 | 178 |
    179 |
    180 |
    181 | ); 182 | } 183 | } 184 | 185 | Item.propTypes = propTypes; 186 | 187 | export default Item; 188 | -------------------------------------------------------------------------------- /client/src/components/Checkout-step-two.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { 4 | Row, 5 | Col, 6 | Input, 7 | Badge, 8 | Button, 9 | ListGroupItem, 10 | Collapse, 11 | Label, 12 | Alert, 13 | FormGroup 14 | } from 'reactstrap'; 15 | 16 | const propTypes = { 17 | addUserAddress: PropTypes.func.isRequired, 18 | step2: PropTypes.bool.isRequired, 19 | step2Unlock: PropTypes.bool.isRequired, 20 | firstName: PropTypes.string.isRequired, 21 | lastName: PropTypes.string.isRequired, 22 | country: PropTypes.string.isRequired, 23 | city: PropTypes.string.isRequired, 24 | province: PropTypes.string.isRequired, 25 | postalCode: PropTypes.number.isRequired, 26 | phoneNumber: PropTypes.number.isRequired, 27 | address1: PropTypes.string.isRequired, 28 | address2: PropTypes.string.isRequired, 29 | shippingMethod: PropTypes.number.isRequired, 30 | formIsValid: PropTypes.bool.isRequired, 31 | onChangeFirstName: PropTypes.func.isRequired, 32 | onChangeLastName: PropTypes.func.isRequired, 33 | onChangeCountry: PropTypes.func.isRequired, 34 | onChangeCity: PropTypes.func.isRequired, 35 | onChangeProvince: PropTypes.func.isRequired, 36 | onChangePostalCode: PropTypes.func.isRequired, 37 | onChangePhoneNumber: PropTypes.func.isRequired, 38 | onChangeAdress1: PropTypes.func.isRequired, 39 | onChangeAdress2: PropTypes.func.isRequired, 40 | onChangeShipppingMethod: PropTypes.func.isRequired, 41 | toggle: PropTypes.func.isRequired, 42 | stepsUnlock: PropTypes.func.isRequired, 43 | formValidator: PropTypes.func.isRequired 44 | }; 45 | 46 | const CheckoutStepTwo = ({ 47 | styles, 48 | step2, 49 | step2Unlock, 50 | toggle, 51 | stepsUnlock, 52 | onChangeFirstName, 53 | onChangeLastName, 54 | onChangeCountry, 55 | onChangeCity, 56 | onChangeProvince, 57 | onChangePostalCode, 58 | onChangePhoneNumber, 59 | onChangeAdress1, 60 | onChangeAdress2, 61 | onChangeShipppingMethod, 62 | firstName, 63 | lastName, 64 | country, 65 | city, 66 | province, 67 | postalCode, 68 | phoneNumber, 69 | address1, 70 | address2, 71 | shippingMethod, 72 | formValidator, 73 | formIsValid, 74 | addUserAddress 75 | }) => { 76 | 77 | const validatorClient = { 78 | firstName: firstName.length > 2 && typeof firstName === 'string', 79 | lastName: lastName.length > 2 && typeof lastName === 'string', 80 | country: typeof country === 'string', 81 | city: city.length > 2 && typeof city === 'string', 82 | province: province.length > 2 && typeof province === 'string', 83 | postalCode: typeof postalCode === 'number', 84 | phoneNumber: typeof phoneNumber === 'number', 85 | address1: address1.length > 2 && typeof address1 === 'string' 86 | } 87 | 88 | const warningValidator = (x) => { 89 | if(x) { 90 | if([...new Set(Object.entries(validatorClient).map(([k, v]) => (v)))].sort()[0] === false){ 91 | return( 92 | 93 | 94 | Please fill these fields correctly: 95 | {!validatorClient.firstName &&
    First Name
    } 96 | {!validatorClient.lastName &&
    Last Name
    } 97 | {!validatorClient.country &&
    Country
    } 98 | {!validatorClient.city &&
    City
    } 99 | {!validatorClient.province &&
    Province
    } 100 | {!validatorClient.postalCode &&
    Postal Code
    } 101 | {!validatorClient.phoneNumber &&
    Phone Number
    } 102 | {!validatorClient.address1 &&
    Address1 field
    } 103 |
    104 |
    105 | ) 106 | } else { 107 | return( 108 | 109 | Thank you! Click 'continue' 110 | 111 | ) 112 | } 113 | } 114 | } 115 | 116 | return ( 117 |
    118 | 119 |

    step2Unlock && toggle('step2')}> 120 | 2 Shipping {} 121 |

    122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 174 | 175 | 176 | 180 | 181 | 182 | 183 |
    184 | 202 |
    203 |
    204 | {warningValidator(formIsValid)} 205 |
    206 |
    207 |
    208 | ) 209 | } 210 | 211 | CheckoutStepTwo.propTypes = propTypes; 212 | 213 | export default CheckoutStepTwo; 214 | -------------------------------------------------------------------------------- /client/src/style/checkbox.min.css: -------------------------------------------------------------------------------- 1 | .pretty *{box-sizing:border-box}.pretty input:not([type=checkbox]):not([type=radio]){display:none}.pretty{position:relative;display:inline-block;margin-right:1em;white-space:nowrap;line-height:1}.pretty input{position:absolute;left:0;top:0;min-width:1em;width:100%;height:100%;z-index:2;opacity:0;margin:0;padding:0;cursor:pointer}.pretty .state label{position:initial;display:inline-block;font-weight:400;margin:0;text-indent:1.5em;min-width:calc(1em + 2px)}.pretty .state label:after,.pretty .state label:before{content:'';width:calc(1em + 2px);height:calc(1em + 2px);display:block;box-sizing:border-box;border-radius:0;border:1px solid transparent;z-index:0;position:absolute;left:0;top:calc((0% - (100% - 1em)) - 8%);background-color:transparent}.pretty .state label:before{border-color:#bdc3c7}.pretty .state.p-is-hover,.pretty .state.p-is-indeterminate{display:none}@-webkit-keyframes zoom{0%{opacity:0;-webkit-transform:scale(0);transform:scale(0)}}@keyframes zoom{0%{opacity:0;-webkit-transform:scale(0);transform:scale(0)}}@-webkit-keyframes tada{0%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0;-webkit-transform:scale(7);transform:scale(7)}38%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out;opacity:1;-webkit-transform:scale(1);transform:scale(1)}55%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;-webkit-transform:scale(1.5);transform:scale(1.5)}72%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out;-webkit-transform:scale(1);transform:scale(1)}81%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;-webkit-transform:scale(1.24);transform:scale(1.24)}89%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out;-webkit-transform:scale(1);transform:scale(1)}95%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;-webkit-transform:scale(1.04);transform:scale(1.04)}100%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out;-webkit-transform:scale(1);transform:scale(1)}}@keyframes tada{0%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0;-webkit-transform:scale(7);transform:scale(7)}38%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out;opacity:1;-webkit-transform:scale(1);transform:scale(1)}55%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;-webkit-transform:scale(1.5);transform:scale(1.5)}72%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out;-webkit-transform:scale(1);transform:scale(1)}81%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;-webkit-transform:scale(1.24);transform:scale(1.24)}89%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out;-webkit-transform:scale(1);transform:scale(1)}95%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;-webkit-transform:scale(1.04);transform:scale(1.04)}100%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out;-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes jelly{0%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}30%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}40%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}50%{-webkit-transform:scale3d(.85,1.15,1);transform:scale3d(.85,1.15,1)}65%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}75%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}100%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes jelly{0%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}30%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}40%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}50%{-webkit-transform:scale3d(.85,1.15,1);transform:scale3d(.85,1.15,1)}65%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}75%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}100%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@-webkit-keyframes rotate{0%{opacity:0;-webkit-transform:translateZ(-200px) rotate(-45deg);transform:translateZ(-200px) rotate(-45deg)}100%{opacity:1;-webkit-transform:translateZ(0) rotate(0);transform:translateZ(0) rotate(0)}}@keyframes rotate{0%{opacity:0;-webkit-transform:translateZ(-200px) rotate(-45deg);transform:translateZ(-200px) rotate(-45deg)}100%{opacity:1;-webkit-transform:translateZ(0) rotate(0);transform:translateZ(0) rotate(0)}}@-webkit-keyframes pulse{0%{box-shadow:0 0 0 0 #bdc3c7}100%{box-shadow:0 0 0 1.5em rgba(189,195,199,0)}}@keyframes pulse{0%{box-shadow:0 0 0 0 #bdc3c7}100%{box-shadow:0 0 0 1.5em rgba(189,195,199,0)}}.pretty.p-default.p-fill .state label:after{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}.pretty.p-default .state label:after{-webkit-transform:scale(.6);-ms-transform:scale(.6);transform:scale(.6)}.pretty.p-default input:checked~.state label:after{background-color:#bdc3c7!important}.pretty.p-default.p-thick .state label:after,.pretty.p-default.p-thick .state label:before{border-width:calc(1em / 7)}.pretty.p-default.p-thick .state label:after{-webkit-transform:scale(.4)!important;-ms-transform:scale(.4)!important;transform:scale(.4)!important}.pretty.p-icon .state .icon{position:absolute;font-size:1em;width:calc(1em + 2px);height:calc(1em + 2px);left:0;z-index:1;text-align:center;line-height:normal;top:calc((0% - (100% - 1em)) - 8%);border:1px solid transparent;opacity:0}.pretty.p-icon .state .icon:before{margin:0;width:100%;height:100%;text-align:center;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:1}.pretty.p-icon input:checked~.state .icon{opacity:1}.pretty.p-icon input:checked~.state label:before{border-color:#5a656b}.pretty.p-svg .state .svg{position:absolute;font-size:1em;width:calc(1em + 2px);height:calc(1em + 2px);left:0;z-index:1;text-align:center;line-height:normal;top:calc((0% - (100% - 1em)) - 8%);border:1px solid transparent;opacity:0}.pretty.p-svg .state svg{margin:0;width:100%;height:100%;text-align:center;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:1}.pretty.p-svg input:checked~.state .svg{opacity:1}.pretty.p-image .state img{opacity:0;position:absolute;width:calc(1em + 2px);height:calc(1em + 2px);top:0;top:calc((0% - (100% - 1em)) - 8%);left:0;z-index:0;text-align:center;line-height:normal;-webkit-transform:scale(.8);-ms-transform:scale(.8);transform:scale(.8)}.pretty.p-image input:checked~.state img{opacity:1}.pretty.p-switch input{min-width:2em}.pretty.p-switch .state{position:relative}.pretty.p-switch .state:before{content:'';border:1px solid #bdc3c7;border-radius:60px;width:2em;box-sizing:unset;height:calc(1em + 2px);position:absolute;top:0;top:calc((0% - (100% - 1em)) - 16%);z-index:0;transition:all .5s ease}.pretty.p-switch .state label{text-indent:2.5em}.pretty.p-switch .state label:after,.pretty.p-switch .state label:before{transition:all .5s ease;border-radius:100%;left:0;border-color:transparent;-webkit-transform:scale(.8);-ms-transform:scale(.8);transform:scale(.8)}.pretty.p-switch .state label:after{background-color:#bdc3c7!important}.pretty.p-switch input:checked~.state:before{border-color:#5a656b}.pretty.p-switch input:checked~.state label:before{opacity:0}.pretty.p-switch input:checked~.state label:after{background-color:#5a656b!important;left:1em}.pretty.p-switch.p-fill input:checked~.state:before{border-color:#5a656b;background-color:#5a656b!important}.pretty.p-switch.p-fill input:checked~.state label:before{opacity:0}.pretty.p-switch.p-fill input:checked~.state label:after{background-color:#fff!important;left:1em}.pretty.p-switch.p-slim .state:before{height:.1em;background:#bdc3c7!important;top:calc(50% - .1em)}.pretty.p-switch.p-slim input:checked~.state:before{border-color:#5a656b;background-color:#5a656b!important}.pretty.p-has-hover input:hover~.state:not(.p-is-hover){display:none}.pretty.p-has-hover input:hover~.state.p-is-hover{display:block}.pretty.p-has-hover input:hover~.state.p-is-hover .icon{display:block}.pretty.p-has-focus input:focus~.state label:before{box-shadow:0 0 3px 0 #bdc3c7}.pretty.p-has-indeterminate input[type=checkbox]:indeterminate~.state:not(.p-is-indeterminate){display:none}.pretty.p-has-indeterminate input[type=checkbox]:indeterminate~.state.p-is-indeterminate{display:block}.pretty.p-has-indeterminate input[type=checkbox]:indeterminate~.state.p-is-indeterminate .icon{display:block;opacity:1}.pretty.p-toggle .state.p-on{opacity:0;display:none}.pretty.p-toggle .state .icon,.pretty.p-toggle .state .svg,.pretty.p-toggle .state img,.pretty.p-toggle .state.p-off{opacity:1;display:inherit}.pretty.p-toggle .state.p-off .icon{color:#bdc3c7}.pretty.p-toggle input:checked~.state.p-on{opacity:1;display:inherit}.pretty.p-toggle input:checked~.state.p-off{opacity:0;display:none}.pretty.p-plain input:checked~.state label:before,.pretty.p-plain.p-toggle .state label:before{content:none}.pretty.p-plain.p-plain .icon{-webkit-transform:scale(1.1);-ms-transform:scale(1.1);transform:scale(1.1)}.pretty.p-round .state label:after,.pretty.p-round .state label:before{border-radius:100%}.pretty.p-round.p-icon .state .icon{border-radius:100%;overflow:hidden}.pretty.p-round.p-icon .state .icon:before{-webkit-transform:scale(.8);-ms-transform:scale(.8);transform:scale(.8)}.pretty.p-curve .state label:after,.pretty.p-curve .state label:before{border-radius:20%}.pretty.p-smooth .icon,.pretty.p-smooth .svg,.pretty.p-smooth label:after,.pretty.p-smooth label:before{transition:all .5s ease}.pretty.p-smooth input:checked+.state label:after{transition:all .3s ease}.pretty.p-smooth input:checked+.state .icon,.pretty.p-smooth input:checked+.state .svg,.pretty.p-smooth input:checked+.state img{-webkit-animation:zoom .2s ease;animation:zoom .2s ease}.pretty.p-smooth.p-default input:checked+.state label:after{-webkit-animation:zoom .2s ease;animation:zoom .2s ease}.pretty.p-smooth.p-plain input:checked+.state label:before{content:'';-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);transition:all .5s ease}.pretty.p-tada:not(.p-default) input:checked+.state .icon,.pretty.p-tada:not(.p-default) input:checked+.state .svg,.pretty.p-tada:not(.p-default) input:checked+.state img,.pretty.p-tada:not(.p-default) input:checked+.state label:after,.pretty.p-tada:not(.p-default) input:checked+.state label:before{-webkit-animation:tada .7s cubic-bezier(.25,.46,.45,.94) 1 alternate;animation:tada .7s cubic-bezier(.25,.46,.45,.94) 1 alternate;opacity:1}.pretty.p-jelly:not(.p-default) input:checked+.state .icon,.pretty.p-jelly:not(.p-default) input:checked+.state .svg,.pretty.p-jelly:not(.p-default) input:checked+.state img,.pretty.p-jelly:not(.p-default) input:checked+.state label:after,.pretty.p-jelly:not(.p-default) input:checked+.state label:before{-webkit-animation:jelly .7s cubic-bezier(.25,.46,.45,.94);animation:jelly .7s cubic-bezier(.25,.46,.45,.94);opacity:1}.pretty.p-jelly:not(.p-default) input:checked+.state label:before{border-color:transparent}.pretty.p-rotate:not(.p-default) input:checked~.state .icon,.pretty.p-rotate:not(.p-default) input:checked~.state .svg,.pretty.p-rotate:not(.p-default) input:checked~.state img,.pretty.p-rotate:not(.p-default) input:checked~.state label:after,.pretty.p-rotate:not(.p-default) input:checked~.state label:before{-webkit-animation:rotate .7s cubic-bezier(.25,.46,.45,.94);animation:rotate .7s cubic-bezier(.25,.46,.45,.94);opacity:1}.pretty.p-rotate:not(.p-default) input:checked~.state label:before{border-color:transparent}.pretty.p-pulse:not(.p-switch) input:checked~.state label:before{-webkit-animation:pulse 1s;animation:pulse 1s}.pretty input[disabled]{cursor:not-allowed;display:none}.pretty input[disabled]~*{opacity:.5}.pretty.p-locked input{display:none;cursor:not-allowed}.pretty input:checked~.state.p-primary label:after,.pretty.p-toggle .state.p-primary label:after{background-color:#428bca!important}.pretty input:checked~.state.p-primary .icon,.pretty input:checked~.state.p-primary .svg,.pretty.p-toggle .state.p-primary .icon,.pretty.p-toggle .state.p-primary .svg{color:#fff;stroke:#fff}.pretty input:checked~.state.p-primary-o label:before,.pretty.p-toggle .state.p-primary-o label:before{border-color:#428bca}.pretty input:checked~.state.p-primary-o label:after,.pretty.p-toggle .state.p-primary-o label:after{background-color:transparent}.pretty input:checked~.state.p-primary-o .icon,.pretty input:checked~.state.p-primary-o .svg,.pretty input:checked~.state.p-primary-o svg,.pretty.p-toggle .state.p-primary-o .icon,.pretty.p-toggle .state.p-primary-o .svg,.pretty.p-toggle .state.p-primary-o svg{color:#428bca;stroke:#428bca}.pretty.p-default:not(.p-fill) input:checked~.state.p-primary-o label:after{background-color:#428bca!important}.pretty.p-switch input:checked~.state.p-primary:before{border-color:#428bca}.pretty.p-switch.p-fill input:checked~.state.p-primary:before{background-color:#428bca!important}.pretty.p-switch.p-slim input:checked~.state.p-primary:before{border-color:#245682;background-color:#245682!important}.pretty input:checked~.state.p-info label:after,.pretty.p-toggle .state.p-info label:after{background-color:#5bc0de!important}.pretty input:checked~.state.p-info .icon,.pretty input:checked~.state.p-info .svg,.pretty.p-toggle .state.p-info .icon,.pretty.p-toggle .state.p-info .svg{color:#fff;stroke:#fff}.pretty input:checked~.state.p-info-o label:before,.pretty.p-toggle .state.p-info-o label:before{border-color:#5bc0de}.pretty input:checked~.state.p-info-o label:after,.pretty.p-toggle .state.p-info-o label:after{background-color:transparent}.pretty input:checked~.state.p-info-o .icon,.pretty input:checked~.state.p-info-o .svg,.pretty input:checked~.state.p-info-o svg,.pretty.p-toggle .state.p-info-o .icon,.pretty.p-toggle .state.p-info-o .svg,.pretty.p-toggle .state.p-info-o svg{color:#5bc0de;stroke:#5bc0de}.pretty.p-default:not(.p-fill) input:checked~.state.p-info-o label:after{background-color:#5bc0de!important}.pretty.p-switch input:checked~.state.p-info:before{border-color:#5bc0de}.pretty.p-switch.p-fill input:checked~.state.p-info:before{background-color:#5bc0de!important}.pretty.p-switch.p-slim input:checked~.state.p-info:before{border-color:#2390b0;background-color:#2390b0!important}.pretty input:checked~.state.p-success label:after,.pretty.p-toggle .state.p-success label:after{background-color:#5cb85c!important}.pretty input:checked~.state.p-success .icon,.pretty input:checked~.state.p-success .svg,.pretty.p-toggle .state.p-success .icon,.pretty.p-toggle .state.p-success .svg{color:#fff;stroke:#fff}.pretty input:checked~.state.p-success-o label:before,.pretty.p-toggle .state.p-success-o label:before{border-color:#5cb85c}.pretty input:checked~.state.p-success-o label:after,.pretty.p-toggle .state.p-success-o label:after{background-color:transparent}.pretty input:checked~.state.p-success-o .icon,.pretty input:checked~.state.p-success-o .svg,.pretty input:checked~.state.p-success-o svg,.pretty.p-toggle .state.p-success-o .icon,.pretty.p-toggle .state.p-success-o .svg,.pretty.p-toggle .state.p-success-o svg{color:#5cb85c;stroke:#5cb85c}.pretty.p-default:not(.p-fill) input:checked~.state.p-success-o label:after{background-color:#5cb85c!important}.pretty.p-switch input:checked~.state.p-success:before{border-color:#5cb85c}.pretty.p-switch.p-fill input:checked~.state.p-success:before{background-color:#5cb85c!important}.pretty.p-switch.p-slim input:checked~.state.p-success:before{border-color:#357935;background-color:#357935!important}.pretty input:checked~.state.p-warning label:after,.pretty.p-toggle .state.p-warning label:after{background-color:#f0ad4e!important}.pretty input:checked~.state.p-warning .icon,.pretty input:checked~.state.p-warning .svg,.pretty.p-toggle .state.p-warning .icon,.pretty.p-toggle .state.p-warning .svg{color:#fff;stroke:#fff}.pretty input:checked~.state.p-warning-o label:before,.pretty.p-toggle .state.p-warning-o label:before{border-color:#f0ad4e}.pretty input:checked~.state.p-warning-o label:after,.pretty.p-toggle .state.p-warning-o label:after{background-color:transparent}.pretty input:checked~.state.p-warning-o .icon,.pretty input:checked~.state.p-warning-o .svg,.pretty input:checked~.state.p-warning-o svg,.pretty.p-toggle .state.p-warning-o .icon,.pretty.p-toggle .state.p-warning-o .svg,.pretty.p-toggle .state.p-warning-o svg{color:#f0ad4e;stroke:#f0ad4e}.pretty.p-default:not(.p-fill) input:checked~.state.p-warning-o label:after{background-color:#f0ad4e!important}.pretty.p-switch input:checked~.state.p-warning:before{border-color:#f0ad4e}.pretty.p-switch.p-fill input:checked~.state.p-warning:before{background-color:#f0ad4e!important}.pretty.p-switch.p-slim input:checked~.state.p-warning:before{border-color:#c77c11;background-color:#c77c11!important}.pretty input:checked~.state.p-danger label:after,.pretty.p-toggle .state.p-danger label:after{background-color:#d9534f!important}.pretty input:checked~.state.p-danger .icon,.pretty input:checked~.state.p-danger .svg,.pretty.p-toggle .state.p-danger .icon,.pretty.p-toggle .state.p-danger .svg{color:#fff;stroke:#fff}.pretty input:checked~.state.p-danger-o label:before,.pretty.p-toggle .state.p-danger-o label:before{border-color:#d9534f}.pretty input:checked~.state.p-danger-o label:after,.pretty.p-toggle .state.p-danger-o label:after{background-color:transparent}.pretty input:checked~.state.p-danger-o .icon,.pretty input:checked~.state.p-danger-o .svg,.pretty input:checked~.state.p-danger-o svg,.pretty.p-toggle .state.p-danger-o .icon,.pretty.p-toggle .state.p-danger-o .svg,.pretty.p-toggle .state.p-danger-o svg{color:#d9534f;stroke:#d9534f}.pretty.p-default:not(.p-fill) input:checked~.state.p-danger-o label:after{background-color:#d9534f!important}.pretty.p-switch input:checked~.state.p-danger:before{border-color:#d9534f}.pretty.p-switch.p-fill input:checked~.state.p-danger:before{background-color:#d9534f!important}.pretty.p-switch.p-slim input:checked~.state.p-danger:before{border-color:#a02622;background-color:#a02622!important}.pretty.p-bigger .icon,.pretty.p-bigger .img,.pretty.p-bigger .svg,.pretty.p-bigger label:after,.pretty.p-bigger label:before{font-size:1.2em!important;top:calc((0% - (100% - 1em)) - 35%)!important}.pretty.p-bigger label{text-indent:1.7em}@media print{.pretty .state .icon,.pretty .state label:after,.pretty .state label:before,.pretty .state:before{color-adjust:exact;-webkit-print-color-adjust:exact;print-color-adjust:exact}} --------------------------------------------------------------------------------