├── public ├── favicon.ico ├── images │ ├── Screenshot (17).png │ ├── Screenshot (18).png │ ├── Screenshot (19).png │ ├── Screenshot (20).png │ └── Screenshot (21).png ├── manifest.json └── index.html ├── src ├── components │ ├── header │ │ ├── stylesheets │ │ │ ├── dropList.css │ │ │ ├── userHeader.module.sass │ │ │ ├── menu.module.sass │ │ │ ├── search.module.sass │ │ │ └── header.module.sass │ │ ├── components │ │ │ ├── DropList.js │ │ │ ├── Search.js │ │ │ ├── UserHeader.js │ │ │ └── Menu.js │ │ ├── headerContainer.js │ │ └── Header.js │ ├── loadingAnimation │ │ ├── loading.module.sass │ │ └── index.js │ └── autoComplete │ │ ├── AutoCompleteContainer.js │ │ ├── auto.module.sass │ │ └── AutoComplete.js ├── assets │ └── images │ │ ├── background.jpg │ │ ├── loading-bars.svg │ │ ├── filter.svg │ │ └── checkmark.svg ├── pages │ ├── loginsignin │ │ ├── utils │ │ │ ├── capitalizeString.js │ │ │ └── validation.js │ │ ├── components │ │ │ ├── footer.js │ │ │ ├── Button.js │ │ │ ├── Base.js │ │ │ └── FormInput.js │ │ ├── LoginContainer.js │ │ ├── SigninContainer.js │ │ ├── stylesheets │ │ │ ├── loginsignin.module.sass │ │ │ ├── base.module.sass │ │ │ └── formInput.module.sass │ │ ├── Login.js │ │ ├── Signin.js │ │ └── LoginSignin.js │ ├── dashboard │ │ ├── utils │ │ │ ├── findPropWithRegex.js │ │ │ └── generateFilterString.js │ │ ├── components │ │ │ ├── Selection.js │ │ │ ├── Checkbox.js │ │ │ ├── SubSelection.js │ │ │ ├── Product.js │ │ │ ├── Filter_md.js │ │ │ └── Filter.js │ │ ├── stylesheets │ │ │ ├── product.module.sass │ │ │ ├── selection.module.sass │ │ │ ├── dashboard.module.sass │ │ │ ├── sub_selection.module.sass │ │ │ ├── filter.module.sass │ │ │ ├── checkbox.module.sass │ │ │ └── filter_md.module.sass │ │ ├── DashboardContainer.js │ │ └── Dashboard.js │ ├── checkoutCancel │ │ └── CheckoutCancel.js │ ├── checkout │ │ ├── utils │ │ │ └── calcPrice.js │ │ ├── stylesheets │ │ │ ├── subtotal.module.sass │ │ │ ├── table.module.sass │ │ │ └── checkout.module.sass │ │ ├── checkoutContainer.js │ │ ├── components │ │ │ ├── CheckoutTable.js │ │ │ └── Subtotal.js │ │ └── Checkout.js │ ├── productOverview │ │ ├── utils │ │ │ └── mergeProductAndVariants.js │ │ ├── stylesheets │ │ │ ├── sizes.module.sass │ │ │ ├── variants.module.sass │ │ │ └── productOverview.module.sass │ │ ├── ProductOverviewContainer.js │ │ ├── components │ │ │ ├── Sizes.js │ │ │ └── Variants.js │ │ └── ProductOverview.js │ ├── checkoutSuccess │ │ ├── utils │ │ │ └── mapSearchURL.js │ │ ├── CheckoutSuccessContainer.js │ │ ├── stylesheet │ │ │ └── checkoutSuccess.module.sass │ │ └── CheckoutSuccess.js │ └── shoppingBag │ │ ├── ShoppingBagContainer.js │ │ ├── stylesheets │ │ ├── table.module.sass │ │ └── shoppingBag.module.sass │ │ ├── ShoppingBag.js │ │ └── components │ │ └── Table.js ├── configs │ └── paypalConfig.js ├── modules │ ├── Navigation │ │ └── index.js │ ├── Auth │ │ └── index.js │ ├── mediaQuery │ │ ├── _app.sass │ │ └── index.js │ └── serverCall │ │ └── index.js ├── App.test.js ├── index.css ├── redux │ ├── store │ │ └── index.js │ ├── reducer │ │ ├── index.js │ │ ├── signinReducer.js │ │ ├── departmentReducer.js │ │ ├── filterReducer.js │ │ ├── variantsReducer.js │ │ ├── tokenReducer.js │ │ ├── checkoutReducer.js │ │ ├── cartReducer.js │ │ └── productReducer.js │ └── action │ │ ├── filterAction.js │ │ ├── departmentAction.js │ │ ├── variantsAction.js │ │ ├── signinAction.js │ │ ├── tokenAction.js │ │ ├── cartAction.js │ │ ├── checkoutAction.js │ │ └── productAction.js ├── index.js ├── App.css ├── App.js ├── logo.svg └── serviceWorker.js ├── .gitignore ├── package.json ├── LICENSE └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levelopers/Ecommerce-Reactjs/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/components/header/stylesheets/dropList.css: -------------------------------------------------------------------------------- 1 | .nav-link { 2 | padding: 0; 3 | color: white; 4 | } -------------------------------------------------------------------------------- /src/assets/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levelopers/Ecommerce-Reactjs/HEAD/src/assets/images/background.jpg -------------------------------------------------------------------------------- /public/images/Screenshot (17).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levelopers/Ecommerce-Reactjs/HEAD/public/images/Screenshot (17).png -------------------------------------------------------------------------------- /public/images/Screenshot (18).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levelopers/Ecommerce-Reactjs/HEAD/public/images/Screenshot (18).png -------------------------------------------------------------------------------- /public/images/Screenshot (19).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levelopers/Ecommerce-Reactjs/HEAD/public/images/Screenshot (19).png -------------------------------------------------------------------------------- /public/images/Screenshot (20).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levelopers/Ecommerce-Reactjs/HEAD/public/images/Screenshot (20).png -------------------------------------------------------------------------------- /public/images/Screenshot (21).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levelopers/Ecommerce-Reactjs/HEAD/public/images/Screenshot (21).png -------------------------------------------------------------------------------- /src/pages/loginsignin/utils/capitalizeString.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const capitalizeString=(str)=>{ 4 | return str.charAt(0).toUpperCase()+str.slice(1) 5 | } 6 | 7 | export default capitalizeString -------------------------------------------------------------------------------- /src/pages/dashboard/utils/findPropWithRegex.js: -------------------------------------------------------------------------------- 1 | export default (object,regex)=>{ 2 | for (const key in object) { 3 | if (key.match(regex)) { 4 | return key 5 | } 6 | } 7 | return undefined 8 | } -------------------------------------------------------------------------------- /src/pages/checkoutCancel/CheckoutCancel.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Redirect } from 'react-router-dom'; 3 | 4 | export default function CheckoutCancel() { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /src/configs/paypalConfig.js: -------------------------------------------------------------------------------- 1 | // Paypal sandbox api (https://developer.paypal.com/docs/api/overview/) 2 | // replace empty string with your 'Client ID' and 'Secret' 3 | export default { 4 | username: '', 5 | password: '' 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/checkout/utils/calcPrice.js: -------------------------------------------------------------------------------- 1 | 2 | const calcPrice=(subtotal)=>{ 3 | return { 4 | taxes:parseFloat((subtotal*0.13).toFixed(2)), 5 | total:parseFloat((subtotal*1.13).toFixed(2)) 6 | } 7 | } 8 | export default calcPrice -------------------------------------------------------------------------------- /src/pages/productOverview/utils/mergeProductAndVariants.js: -------------------------------------------------------------------------------- 1 | export default (product,variants)=>{ 2 | let resultArray=[product||{}] 3 | 4 | for (const v of variants||[]) { 5 | resultArray.push(v||{}) 6 | } 7 | return resultArray 8 | } -------------------------------------------------------------------------------- /src/modules/Navigation/index.js: -------------------------------------------------------------------------------- 1 | 2 | let history 3 | export const registerNav = (ref) => { 4 | history = ref.history 5 | } 6 | 7 | const jumpTo = (uri) => { 8 | history.push(uri) 9 | } 10 | export const go=(uri)=>{ 11 | history.go(uri) 12 | } 13 | export default jumpTo 14 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/pages/loginsignin/components/footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const style={ 4 | fontSize:'15px', 5 | marginBottom:'30px' 6 | } 7 | 8 | export default function footer({content}) { 9 | return ( 10 |
11 | {content} 12 |
13 | ) 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/pages/productOverview/stylesheets/sizes.module.sass: -------------------------------------------------------------------------------- 1 | @import '~bootstrap/scss/_functions' 2 | @import '~bootstrap/scss/_variables' 3 | @import '~bootstrap/scss/mixins/_breakpoints' 4 | 5 | 6 | @include media-breakpoint-down(sm) 7 | .btn 8 | button 9 | font-size: 0.5rem 10 | .item 11 | font-size: 0.5rem!important -------------------------------------------------------------------------------- /src/pages/checkoutSuccess/utils/mapSearchURL.js: -------------------------------------------------------------------------------- 1 | 2 | const mapSearchURL = (searchUrl) => { 3 | const map = new Map() 4 | let queryArr = searchUrl.replace('?', '').split('&') 5 | for (const k of queryArr) { 6 | let pairArr = k.split('=') 7 | map.set(pairArr[0], pairArr[1]) 8 | } 9 | return map 10 | } 11 | export default mapSearchURL -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/components/loadingAnimation/loading.module.sass: -------------------------------------------------------------------------------- 1 | .loading_background 2 | position: absolute 3 | top: 0 4 | left: 0 5 | width: 100vw 6 | height: 100vh 7 | background: rgba(125,125,125,0.5) 8 | color: blue 9 | display: flex 10 | justify-content: center 11 | align-items: center 12 | z-index: 2 13 | 14 | .svg 15 | width: 10% 16 | height: 10% 17 | // margin-top: 20% -------------------------------------------------------------------------------- /src/components/loadingAnimation/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import loading_animation from '../../assets/images/loading-bars.svg' 3 | import styles from './loading.module.sass' 4 | export default function index() { 5 | return ( 6 |
7 | 8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/loginsignin/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import {connect } from 'react-redux' 2 | import Login from './Login' 3 | import {postToken} from '../../redux/action/tokenAction' 4 | const mapDispatchToProps={ 5 | postToken 6 | } 7 | const mapStoreToProps=state=>({ 8 | login_loading:state.token.token_loading, 9 | login_error:state.token.error 10 | }) 11 | 12 | export default connect(mapStoreToProps,mapDispatchToProps)(Login) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | /src/configs 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /src/pages/loginsignin/SigninContainer.js: -------------------------------------------------------------------------------- 1 | import Signin from './Signin' 2 | import { connect } from 'react-redux' 3 | import { signin } from '../../redux/action/signinAction' 4 | 5 | const mapDispatchToProps = { 6 | signin 7 | } 8 | const mapStoreToProps = state => ({ 9 | signin_loading: state.signin.signin_loading, 10 | signin_error: state.signin.error 11 | }) 12 | 13 | export default connect(mapStoreToProps, mapDispatchToProps)(Signin) 14 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/loginsignin/stylesheets/loginsignin.module.sass: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto') 2 | 3 | .outbox 4 | width: 100vw 5 | height: 100vh 6 | background-image: url(../../../assets/images/background.jpg) 7 | background-size: cover 8 | display: flex 9 | justify-content: center 10 | 11 | .box 12 | width: 20% 13 | min-width: 300px 14 | max-height: 50% 15 | margin-top: 5% 16 | border-radius: 5px 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/pages/loginsignin/stylesheets/base.module.sass: -------------------------------------------------------------------------------- 1 | .outbox 2 | display: flex 3 | flex-direction: column 4 | align-items: center 5 | background: white 6 | 7 | .logo 8 | width: 200px 9 | height: 130px 10 | background: rgb(238,130,238) 11 | font-size: 50px 12 | color: white 13 | 14 | .title_style 15 | color: blue 16 | width: 80% 17 | font-size: 20px 18 | 19 | .border_style 20 | width: 80% 21 | border-bottom: 1px solid blue 22 | margin-bottom: 5px 23 | -------------------------------------------------------------------------------- /src/redux/store/index.js: -------------------------------------------------------------------------------- 1 | import rootReducer from '../reducer' 2 | import {createStore, applyMiddleware, compose} from 'redux' 3 | import thunk from 'redux-thunk' 4 | 5 | const initialState={} 6 | const middlewares=[thunk] 7 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 8 | 9 | const store=createStore( 10 | rootReducer, 11 | initialState, 12 | composeEnhancers( 13 | applyMiddleware(...middlewares) 14 | ) 15 | ) 16 | 17 | export default store -------------------------------------------------------------------------------- /src/pages/dashboard/components/Selection.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from '../stylesheets/selection.module.sass' 3 | export default function Selection({ title, selected,category, onChange }) { 4 | return ( 5 |
onChange(e,category, title)} 8 | > 9 |
10 | {title} 11 |
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/dashboard/stylesheets/product.module.sass: -------------------------------------------------------------------------------- 1 | @import "node_modules/bootstrap/scss/functions"; 2 | @import "node_modules/bootstrap/scss/variables"; 3 | @import "node_modules/bootstrap/scss/mixins/_breakpoints"; 4 | 5 | .card 6 | min-width: 150px 7 | 8 | @include media-breakpoint-down(sm) 9 | .title 10 | font-size: 15px 11 | .subtitle 12 | font-size: 10px!important 13 | .color 14 | font-size: 10px 15 | 16 | // .card 17 | 18 | // .image 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/autoComplete/AutoCompleteContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import AutoComplete from './AutoComplete' 3 | import { filter } from '../../redux/action/filterAction' 4 | 5 | const mapStoreToProps = state => ({ 6 | filter_result: state.filter.filter_result, 7 | error: state.filter.error, 8 | loading: state.filter.loading 9 | }) 10 | 11 | const mapDispatchToProps = { 12 | filter 13 | } 14 | 15 | export default connect(mapStoreToProps, mapDispatchToProps)(AutoComplete) -------------------------------------------------------------------------------- /src/modules/Auth/index.js: -------------------------------------------------------------------------------- 1 | class Auth { 2 | constructor() { 3 | this.user_token = JSON.parse(localStorage.getItem('auth'))||{} 4 | } 5 | getToken() { 6 | return this.user_token.token 7 | } 8 | getUserId() { 9 | return this.user_token.user_id 10 | } 11 | setUserToken(new_token) { 12 | this.user_token = new_token 13 | localStorage.setItem('auth', JSON.stringify(new_token)) 14 | } 15 | logout() { 16 | localStorage.removeItem('auth') 17 | } 18 | } 19 | export default new Auth() -------------------------------------------------------------------------------- /src/pages/checkoutSuccess/CheckoutSuccessContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import CheckoutSuccess from './CheckoutSuccess' 3 | import { getPayment } from '../../redux/action/checkoutAction' 4 | 5 | const mapStoreToProps = state => ({ 6 | payment: state.checkout.payment 7 | }) 8 | const mapDispatchToProps = dispatch => ({ 9 | getPayment: (paymentId, payerId) => dispatch(getPayment(paymentId, payerId)) 10 | }) 11 | 12 | export default connect(mapStoreToProps, mapDispatchToProps)(CheckoutSuccess) -------------------------------------------------------------------------------- /src/pages/shoppingBag/ShoppingBagContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import ShoppingBag from './ShoppingBag' 3 | import {getCartByUserId,postCart} from '../../redux/action/cartAction' 4 | 5 | const mapStoreToProps=state=>({ 6 | cart:state.cart.cart 7 | }) 8 | const mapDispatchToProps=dispatch=>({ 9 | getCartByUserId:dispatch(getCartByUserId()), 10 | postCart:(pid,increase,decrease)=>dispatch(postCart(pid,increase,decrease)) 11 | }) 12 | 13 | export default connect(mapStoreToProps,mapDispatchToProps)(ShoppingBag) -------------------------------------------------------------------------------- /src/pages/checkout/stylesheets/subtotal.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../../modules/mediaQuery/app' 2 | 3 | .outbox 4 | display: flex 5 | flex-direction: column 6 | font-size: 1em 7 | 8 | .subtotals 9 | display: flex 10 | flex-direction: column 11 | .row 12 | display: flex 13 | justify-content: space-between 14 | font-size: 1em 15 | color: grey 16 | margin: 0.1em 0 17 | .total 18 | font-size: 1.2em 19 | color: black 20 | font-weight: 500 21 | 22 | @media screen and (max-width: $tablet) 23 | .outbox 24 | font-size: .7rem -------------------------------------------------------------------------------- /src/redux/reducer/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux' 2 | import token from './tokenReducer' 3 | import signin from './signinReducer' 4 | import department from './departmentReducer' 5 | import product from './productReducer' 6 | import variant from './variantsReducer' 7 | import cart from './cartReducer' 8 | import checkout from './checkoutReducer' 9 | import filter from './filterReducer' 10 | 11 | export default combineReducers({ 12 | token, 13 | signin, 14 | department, 15 | product, 16 | variant, 17 | cart, 18 | checkout, 19 | filter 20 | }) -------------------------------------------------------------------------------- /src/pages/dashboard/stylesheets/selection.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../../modules/mediaQuery/app' 2 | 3 | @media screen and (max-width: $tablet) 4 | .sub_title 5 | height: 100% 6 | display: flex 7 | align-items: center 8 | padding: 2px 0 9 | &:hover, &:focus 10 | border-bottom: 1px solid blue 11 | cursor: pointer 12 | 13 | .selected 14 | border-bottom: 1px solid blue 15 | 16 | .title 17 | font-size: .8rem 18 | padding: 0 .5rem 19 | 20 | @media screen and (max-width: $mobileL) 21 | .title 22 | font-size: .5rem 23 | -------------------------------------------------------------------------------- /src/pages/dashboard/DashboardContainer.js: -------------------------------------------------------------------------------- 1 | import { getAllProducts, applyFilters } from '../../redux/action/productAction' 2 | import { connect } from 'react-redux' 3 | import Dashboard from './Dashboard' 4 | const mapStoreToProps = state => ({ 5 | products: state.product.products, 6 | loading:state.product.loading 7 | }) 8 | const mapDispatchToProps = dispatch => ({ 9 | getAllProducts: ()=>dispatch(getAllProducts()), 10 | applyFilters:(filter_string)=>dispatch(applyFilters(filter_string)) 11 | }) 12 | 13 | export default connect(mapStoreToProps, mapDispatchToProps)(Dashboard) -------------------------------------------------------------------------------- /src/pages/dashboard/components/Checkbox.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from '../stylesheets/checkbox.module.sass' 3 | 4 | export default function Checkbox({ onChange, name, category, isChecked }) { 5 | return ( 6 |
7 | onChange(e, category)} 12 | checked={isChecked} 13 | /> 14 | 18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | import {Provider} from 'react-redux' 7 | import store from './redux/store' 8 | 9 | ReactDOM.render(, document.getElementById('root')); 10 | 11 | // If you want your app to work offline and load faster, you can change 12 | // unregister() to register() below. Note this comes with some pitfalls. 13 | // Learn more about service workers: https://bit.ly/CRA-PWA 14 | serviceWorker.unregister(); 15 | -------------------------------------------------------------------------------- /src/pages/loginsignin/stylesheets/formInput.module.sass: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans') 2 | 3 | .outbox 4 | display: flex 5 | flex-direction: column 6 | width: 70% 7 | // height: 70px 8 | margin-bottom: 5px 9 | 10 | .label 11 | font-family: 'Open Sans', sans-serif 12 | font-size: 15px 13 | label 14 | margin: 0 15 | 16 | .input 17 | width: 100% 18 | margin-bottom: 3px 19 | input 20 | width: 100% 21 | height: 100% 22 | border-radius: 5px 23 | border: 1px solid grey 24 | padding: 0 0.5rem 25 | 26 | .errMsg 27 | color: red 28 | font-size: 15px -------------------------------------------------------------------------------- /src/pages/productOverview/ProductOverviewContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import ProductOverview from './ProductOverview' 3 | import {getProduct} from '../../redux/action/productAction' 4 | import {getVariantsByProductId} from '../../redux/action/variantsAction' 5 | import {postCart} from '../../redux/action/cartAction' 6 | 7 | const mapStoreToProps=state=>({ 8 | product:state.product.product, 9 | variants:state.variant.variants 10 | }) 11 | const mapDispatchToProps={ 12 | getProduct, 13 | getVariantsByProductId, 14 | postCart 15 | } 16 | 17 | export default connect(mapStoreToProps,mapDispatchToProps)(ProductOverview) -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/header/stylesheets/userHeader.module.sass: -------------------------------------------------------------------------------- 1 | .outbox 2 | display: flex 3 | justify-content: flex-end 4 | background: rgb(65,65,65) 5 | height: 4vh 6 | 7 | .tag 8 | padding: 0.5rem 9 | white-space: nowrap 10 | color: white 11 | background: #5E7264 12 | display: flex 13 | align-items: center 14 | justify-content: center 15 | &:hover 16 | cursor: pointer 17 | color: #0056b3 18 | 19 | .loggout 20 | display: flex 21 | align-items: center 22 | margin-right: 2rem 23 | margin-left: 1rem 24 | 25 | .login 26 | color: white 27 | font-size: 1rem 28 | &:hover 29 | cursor: pointer 30 | color: blue -------------------------------------------------------------------------------- /src/components/header/components/DropList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NavDropdown } from 'react-bootstrap' 3 | import jumpTo from '../../../modules/Navigation' 4 | import '../stylesheets/dropList.css' 5 | 6 | export default function DropList({ department, categories,clickCategory }) { 7 | return ( 8 | 9 | {categories && categories.map(c => 10 | { 12 | clickCategory(c) 13 | jumpTo('/dashboard') 14 | }} 15 | key={c}>{c} 16 | 17 | )} 18 | 19 | ) 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/pages/checkout/checkoutContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Checkout from './Checkout' 3 | import {getCheckoutUrl, getPaypalUrl} from '../../redux/action/checkoutAction' 4 | import {getCartByUserId} from '../../redux/action/cartAction' 5 | 6 | const mapStoreToProps = state => ({ 7 | cart:state.cart.cart, 8 | url:state.checkout.approval_url, 9 | name:state.token.user_token.user_name 10 | }) 11 | const mapDispatchToProps = dispatch => ({ 12 | getCheckoutUrl:(cartId)=>dispatch(getCheckoutUrl(cartId)), 13 | getCartByUserId:()=>dispatch(getCartByUserId()) 14 | }) 15 | 16 | export default connect(mapStoreToProps, mapDispatchToProps)(Checkout) -------------------------------------------------------------------------------- /src/pages/loginsignin/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const outStyle={ 4 | width:'80%', 5 | height:'30px', 6 | marginBottom:'10px', 7 | marginTop:'5px' 8 | } 9 | const btnStyle={ 10 | width:'100%', 11 | height:'100%', 12 | borderRadius:'7px', 13 | background:'rgb(0,100,255)', 14 | color:'white', 15 | fontFamily:'Roboto sans-serif', 16 | fontSize:'15px' 17 | } 18 | 19 | export default function Button({ button_title,onClick }) { 20 | return ( 21 |
22 | 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/productOverview/components/Sizes.js: -------------------------------------------------------------------------------- 1 | import { Dropdown, DropdownButton } from 'react-bootstrap' 2 | import React from 'react' 3 | import styles from '../stylesheets/sizes.module.sass' 4 | export default function Sizes({ sizes, selectedSize, clickSize }) { 5 | return ( 6 |
7 | 8 | {sizes.slice(1).length > 0 9 | ? sizes.slice(1).map(s => 10 | clickSize(s)} key={s}>{s} 11 | ) 12 | : "no more sizes" 13 | } 14 | 15 |
16 | ) 17 | } -------------------------------------------------------------------------------- /src/redux/action/filterAction.js: -------------------------------------------------------------------------------- 1 | import serverCall from '../../modules/serverCall' 2 | 3 | export const filter=(text)=>dispatch=>{ 4 | dispatch({ 5 | type:FILTER_BEGIN, 6 | }) 7 | return serverCall({ 8 | method:'GET', 9 | url:`/filter?query=${text}` 10 | }) 11 | .then(res=>{ 12 | dispatch({ 13 | type: FILTER_SUCCESS, 14 | payload: res 15 | }) 16 | return res 17 | }) 18 | .catch(error=>{ 19 | dispatch({ 20 | type: FILTER_FAIL, 21 | payload: {error} 22 | }) 23 | return error 24 | }) 25 | } 26 | 27 | export const FILTER_BEGIN='FILTER_BEGIN' 28 | export const FILTER_SUCCESS='FILTER_SUCCESS' 29 | export const FILTER_FAIL='FILTER_FAIL' -------------------------------------------------------------------------------- /src/pages/checkout/components/CheckoutTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from '../stylesheets/table.module.sass' 3 | 4 | export default function CheckoutTable({ items }) { 5 | return ( 6 |
7 | {Object.keys(items).map(i => 8 |
9 |
10 | 11 |
12 |
13 | {items[i].item.title} 14 |
15 |
16 | ${items[i].item.price} X {items[i].qty} 17 |
18 |
19 | )} 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/components/header/headerContainer.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import Header from './Header' 3 | import {getDepartments} from '../../redux/action/departmentAction' 4 | import {getProductsByCategory,getAllProducts,search} from '../../redux/action/productAction' 5 | 6 | 7 | const mapStoreToProps=state=>({ 8 | user_token:state.token.user_token, 9 | departments:state.department.departments, 10 | }) 11 | 12 | const mapDispatchToProps=dispatch=>({ 13 | getDepartments:dispatch(getDepartments()), 14 | search:(t)=>dispatch(search(t)), 15 | getProductsByCategory:(c)=>dispatch(getProductsByCategory(c)), 16 | getAllProducts:()=>dispatch(getAllProducts()) 17 | }) 18 | 19 | export default connect(mapStoreToProps,mapDispatchToProps)(Header) -------------------------------------------------------------------------------- /src/pages/dashboard/components/SubSelection.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from '../stylesheets/sub_selection.module.sass' 3 | 4 | export default function SubSelection({ selected_arr, onClick, content, category }) { 5 | return ( 6 |
7 |
8 | {content.map(c => { 9 | c = c.toUpperCase() 10 | return ( 11 |
onClick(e, category, c)}> 15 | {c} 16 |
17 | ) 18 | })} 19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/components/header/stylesheets/menu.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../../modules/mediaQuery/app' 2 | 3 | 4 | .outbox 5 | display: flex 6 | margin: auto 7 | color: white !important 8 | 9 | .lists 10 | display: flex 11 | word-break: none 12 | white-space: nowrap 13 | 14 | .tag 15 | padding: 0.8rem 16 | white-space: nowrap 17 | display: flex 18 | align-items: center 19 | // color: #007bff 20 | &:hover 21 | cursor: pointer 22 | color: #0056b3 23 | 24 | @media screen and (max-width: $tablet) 25 | .lists 26 | display: flex 27 | flex-direction: column 28 | 29 | .outbox 30 | display: flex 31 | flex-direction: column-reverse 32 | margin: 0 33 | font-size: .8rem 34 | 35 | .tag 36 | border-bottom: 1px solid rgba(0,0,0,.5) -------------------------------------------------------------------------------- /src/redux/action/departmentAction.js: -------------------------------------------------------------------------------- 1 | import serverCall from '../../modules/serverCall' 2 | 3 | export const getDepartments=()=>dispatch=>{ 4 | dispatch({ 5 | type:GET_DEPARTMENTS_BEGIN, 6 | }) 7 | return serverCall({ 8 | method:'GET', 9 | url:'/departments' 10 | }) 11 | .then(res=>{ 12 | dispatch({ 13 | type:GET_DEPARTMENTS_SUCCESS, 14 | payload:res 15 | }) 16 | return res 17 | }) 18 | .catch(error=>{ 19 | dispatch({ 20 | type:GET_DEPARTMENTS_FAIL, 21 | payload:{error} 22 | }) 23 | return error 24 | }) 25 | } 26 | 27 | export const GET_DEPARTMENTS_BEGIN = 'GET_DEPARTMENTS_BEGIN' 28 | export const GET_DEPARTMENTS_SUCCESS='GET_DEPARTMENTS_SUCCESS' 29 | export const GET_DEPARTMENTS_FAIL='GET_DEPARTMENTS_FAIL' -------------------------------------------------------------------------------- /src/redux/reducer/signinReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | POST_SIGNIN_BEGIN, 3 | POST_SIGNIN_SUCCESS, 4 | POST_SIGNIN_FAIL 5 | } from '../action/signinAction' 6 | 7 | const initialState = { 8 | signin_loading: false, 9 | error: {}, 10 | } 11 | 12 | export default (state = initialState, action) => { 13 | switch (action.type) { 14 | case POST_SIGNIN_BEGIN: 15 | return { 16 | ...state, 17 | signin_loading: true 18 | } 19 | case POST_SIGNIN_SUCCESS: 20 | return { 21 | ...state, 22 | signin_loading: false, 23 | } 24 | case POST_SIGNIN_FAIL: 25 | return { 26 | ...state, 27 | signin_loading: false, 28 | error: action.payload.error.response.data 29 | } 30 | default: 31 | return state 32 | } 33 | } -------------------------------------------------------------------------------- /src/pages/dashboard/stylesheets/dashboard.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../../modules/mediaQuery/app' 2 | 3 | .outbox 4 | min-width: 320px 5 | min-height: 568px 6 | max-width: 100vw 7 | max-height: 100vh 8 | 9 | .box 10 | display: flex 11 | justify-content: space-around 12 | margin-top: 12vh 13 | max-width: 100% 14 | 15 | .filter 16 | margin: 1rem 17 | 18 | .products 19 | display: flex 20 | width: 80% 21 | flexWrap: wrap !important 22 | 23 | .product 24 | min-width: 150px 25 | // margin: .5rem 26 | 27 | @media screen and (max-width: $tablet) 28 | .box 29 | display: flex 30 | flex-direction: column 31 | justify-content: space-around 32 | align-items: center 33 | margin-top: 8vh 34 | 35 | .filter 36 | margin: 0 37 | width: 100% 38 | 39 | 40 | .products 41 | width: 100% -------------------------------------------------------------------------------- /src/redux/action/variantsAction.js: -------------------------------------------------------------------------------- 1 | import serverCall from '../../modules/serverCall' 2 | 3 | export const getVariantsByProductId=(productId)=>dispatch=>{ 4 | dispatch({ 5 | type:GET_VARIANTS_QUERY_BEGIN, 6 | }) 7 | return serverCall({ 8 | method:'GET', 9 | url:`/variants?productId=${productId}` 10 | }) 11 | .then(res=>{ 12 | dispatch({ 13 | type: GET_VARIANTS_QUERY_SUCCESS, 14 | payload: res 15 | }) 16 | return res 17 | }) 18 | .catch(error=>{ 19 | dispatch({ 20 | type: GET_VARIANTS_QUERY_FAIL, 21 | payload: {error} 22 | }) 23 | return error 24 | }) 25 | } 26 | 27 | export const GET_VARIANTS_QUERY_BEGIN='GET_VARIANTS_QUERY_BEGIN' 28 | export const GET_VARIANTS_QUERY_SUCCESS='GET_VARIANTS_QUERY_SUCCESS' 29 | export const GET_VARIANTS_QUERY_FAIL='GET_VARIANTS_QUERY_FAIL' -------------------------------------------------------------------------------- /src/redux/reducer/departmentReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_DEPARTMENTS_BEGIN, 3 | GET_DEPARTMENTS_SUCCESS, 4 | GET_DEPARTMENTS_FAIL 5 | } from '../action/departmentAction' 6 | 7 | const initialState = { 8 | loading: false, 9 | departments: null, 10 | error: null, 11 | } 12 | 13 | export default (state = initialState, action) => { 14 | switch (action.type) { 15 | case GET_DEPARTMENTS_BEGIN: 16 | return { 17 | loading: true, 18 | error: null 19 | } 20 | case GET_DEPARTMENTS_SUCCESS: 21 | return { 22 | loading: false, 23 | departments: action.payload.data.departments 24 | } 25 | case GET_DEPARTMENTS_FAIL: 26 | return { 27 | loading: false, 28 | error: action.payload.error.response.data 29 | } 30 | default: 31 | return state 32 | } 33 | } -------------------------------------------------------------------------------- /src/redux/action/signinAction.js: -------------------------------------------------------------------------------- 1 | import serverCall from '../../modules/serverCall' 2 | 3 | export const signin=(fullname,email,password,verifyPassword)=>dispatch=>{ 4 | dispatch({ 5 | type: POST_SIGNIN_BEGIN, 6 | }) 7 | return serverCall({ 8 | method:'POST', 9 | url:'/users/signin', 10 | data:{ 11 | fullname,email,password,verifyPassword 12 | } 13 | }) 14 | .then(res=>{ 15 | dispatch({ 16 | type:POST_SIGNIN_SUCCESS, 17 | payload:res 18 | }) 19 | return res 20 | }) 21 | .catch(error=>{ 22 | dispatch({ 23 | type:POST_SIGNIN_FAIL, 24 | payload:{error} 25 | }) 26 | throw error 27 | }) 28 | } 29 | 30 | export const POST_SIGNIN_BEGIN='POST_SIGNIN_BEGIN' 31 | export const POST_SIGNIN_SUCCESS='POST_SIGNIN_SUCCESS' 32 | export const POST_SIGNIN_FAIL='POST_SIGNIN_FAIL' 33 | -------------------------------------------------------------------------------- /src/redux/reducer/filterReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | FILTER_BEGIN, 3 | FILTER_SUCCESS, 4 | FILTER_FAIL 5 | } from '../action/filterAction' 6 | 7 | const initialState = { 8 | loading: false, 9 | filter_result: null, 10 | error: null, 11 | } 12 | 13 | export default (state = initialState, action) => { 14 | switch (action.type) { 15 | case FILTER_BEGIN: 16 | return { 17 | ...state, 18 | loading: true, 19 | error: null 20 | } 21 | case FILTER_SUCCESS: 22 | return { 23 | ...state, 24 | loading: false, 25 | filter_result: action.payload.data.filter 26 | } 27 | 28 | case FILTER_FAIL: 29 | return { 30 | ...state, 31 | loading: false, 32 | error: action.payload.error.response.data 33 | } 34 | default: 35 | return state 36 | } 37 | } -------------------------------------------------------------------------------- /src/modules/mediaQuery/_app.sass: -------------------------------------------------------------------------------- 1 | //breakpoints 2 | $mobileS: 320px; 3 | $mobileM: 375px;//iphone 4 | $mobileL: 425px;//plus 414px 5 | $tablet: 768px; 6 | $laptop: 1024px; 7 | $laptopL: 1440px; 8 | $desktop: 2560px; 9 | 10 | =device($point) 11 | @if $point == mobileS 12 | @media (min-width: $mobileS) 13 | @content 14 | @else if $point == mobileM 15 | @media (min-width: $mobileM) 16 | @content 17 | @else if $point == mobileL 18 | @media (min-width: $mobileL) 19 | @content 20 | @else if $point == tablet 21 | @media (min-width: $tablet) 22 | @content 23 | @else if $point == laptop 24 | @media (min-width: $laptop) 25 | @content 26 | @else if $point == laptopL 27 | @media (min-width: $laptopL) 28 | @content 29 | @else if $point == desktop 30 | @media (min-width: $desktop) 31 | @content -------------------------------------------------------------------------------- /src/pages/shoppingBag/stylesheets/table.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../../modules/mediaQuery/app' 2 | 3 | 4 | .outbox 5 | font-size: 1rem 6 | .id 7 | font-size: 0.7em 8 | .pic_outbox 9 | max-width: 6em 10 | padding: 0 11 | display: flex 12 | justify-content: center 13 | align-items: center 14 | img 15 | max-width: calc(100% - 1.3em) 16 | 17 | .title_outbox 18 | display: flex 19 | flex-direction: column 20 | width: max-content 21 | margin: 0 22 | 23 | .title 24 | word-wrap: none !important 25 | min-width: 6em 26 | max-width: 8em 27 | margin-bottom: 1em 28 | .qty_outbox 29 | display: flex 30 | flex-wrap: nowrap 31 | 32 | .btn 33 | padding: 0 0.5em 34 | margin: 0 0.5em 35 | 36 | @media screen and (max-width: $tablet) 37 | .outbox 38 | font-size: .7rem 39 | .pic_outbox 40 | font-size: .6rem 41 | .btn 42 | font-size: .5rem 43 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/Product.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Card } from 'react-bootstrap' 3 | import styles from '../stylesheets/product.module.sass' 4 | 5 | export default function Product({ title, color, price, image }) { 6 | return ( 7 | 12 | 13 | 14 | {title} 15 | 16 |
17 | {price} 18 |
19 |
20 | 21 | {color} 22 | 23 |
24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/dashboard/stylesheets/sub_selection.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../../modules/mediaQuery/app' 2 | 3 | @media screen and (max-width: $tablet) 4 | .outbox 5 | width: 100% 6 | min-height: max-content 7 | padding: 0 2rem 8 | 9 | .box 10 | z-index: 2 11 | width: 100% 12 | height: 100% 13 | display: flex 14 | justify-content: space-around 15 | align-items: center 16 | color: black 17 | background: rgba(125,125,125,.2) 18 | border-radius: 8px 19 | flex-wrap: wrap 20 | .content 21 | height: 3rem 22 | display: flex 23 | align-items: center 24 | font-size: .8rem 25 | cursor: pointer 26 | padding: 2px .5rem 27 | white-space: nowrap 28 | 29 | .selected 30 | border-bottom: 1px solid blue 31 | 32 | @media screen and (max-width: $mobileL) 33 | .content 34 | font-size: .5rem 35 | height: 1rem 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/redux/reducer/variantsReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_VARIANTS_QUERY_BEGIN, 3 | GET_VARIANTS_QUERY_SUCCESS, 4 | GET_VARIANTS_QUERY_FAIL 5 | } from '../action/variantsAction' 6 | 7 | const initialState = { 8 | variants: null, 9 | loading: false, 10 | error: null, 11 | } 12 | 13 | export default (state = initialState, action) => { 14 | switch (action.type) { 15 | case GET_VARIANTS_QUERY_BEGIN: 16 | return { 17 | ...state, 18 | loading: true, 19 | error: null 20 | } 21 | case GET_VARIANTS_QUERY_SUCCESS: 22 | return { 23 | ...state, 24 | loading: false, 25 | variants: action.payload.data.variants 26 | } 27 | case GET_VARIANTS_QUERY_FAIL: 28 | return { 29 | ...state, 30 | loading: false, 31 | error: action.payload.error 32 | } 33 | default: 34 | return state 35 | } 36 | } -------------------------------------------------------------------------------- /src/pages/checkout/stylesheets/table.module.sass: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Comfortaa'); 2 | @import '../../../modules/mediaQuery/app' 3 | 4 | .outbox 5 | display: flex 6 | flex-direction: column 7 | 8 | .row 9 | display: flex 10 | justify-content: space-between 11 | margin: 3% 0 12 | .pic 13 | max-width: 6em 14 | margin-right: 1em 15 | display: flex 16 | justify-content: center 17 | align-items: center 18 | overflow: 19 | img 20 | max-width: 70% 21 | border-radius: 10% 22 | 23 | .title 24 | margin: 0 .3em 25 | font-size: 1em 26 | font-family: sans-serif 27 | 28 | .price 29 | margin-left: auto 30 | font-size: 1em 31 | font-family: 'Comfortaa', cursive; 32 | font-weight: 600 33 | 34 | 35 | @media screen and (max-width: $tablet) 36 | .outbox 37 | font-size: .7rem 38 | 39 | .pic 40 | font-size: .6rem 41 | .price 42 | font-size: .5rem -------------------------------------------------------------------------------- /src/components/header/components/Search.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from '../stylesheets/search.module.sass' 3 | import AutoComplete from '../../autoComplete/AutoCompleteContainer' 4 | import jumpTo from '../../../modules/Navigation' 5 | export default function Search({ 6 | search, 7 | onChange, 8 | handleSuggest, 9 | input_value 10 | }) { 11 | return ( 12 |
13 | {/* search input */} 14 |
15 | 19 |
20 |
21 | 28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/dashboard/stylesheets/filter.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../../modules/mediaQuery/app' 2 | 3 | 4 | .outbox 5 | width: 250px 6 | min-width: 100px 7 | background: white 8 | 9 | .box 10 | display: flex 11 | flex-direction: column 12 | 13 | .title 14 | font-size: 1.5rem 15 | background: white 16 | 17 | .title_border 18 | border-bottom: 1px solid green 19 | padding-bottom: .5rem 20 | 21 | .tags 22 | display: flex 23 | flex-wrap: wrap 24 | 25 | .tag 26 | padding: .2rem 27 | background: rgba(0,0,0,.1) 28 | margin: .2rem 29 | display: flex 30 | flex-wrap: nowrap 31 | 32 | .tag_content 33 | margin: 0 .2rem 34 | 35 | .tag_close 36 | padding: 0 .2rem 37 | border-left: 1px solid white 38 | cursor: pointer 39 | 40 | .content 41 | display: flex 42 | flex-direction: column 43 | margin-top: 1rem 44 | 45 | .block 46 | margin-bottom: 1rem 47 | 48 | .sub_title 49 | // font-size: 1rem 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommerce-reactjs", 3 | "version": "1.1.0", 4 | "private": true, 5 | "homepage": "https://zack-ecommerce-reactjs.herokuapp.com/", 6 | "dependencies": { 7 | "axios": "^0.18.0", 8 | "bootstrap": "^4.3.1", 9 | "node-sass": "^4.12.0", 10 | "react": "^16.8.6", 11 | "react-bootstrap": "^1.0.0-beta.8", 12 | "react-dom": "^16.8.6", 13 | "react-redux": "^7.0.2", 14 | "react-responsive": "^6.1.2", 15 | "react-router-dom": "^5.0.0", 16 | "react-scripts": "^3.0.0", 17 | "redux": "^4.0.1", 18 | "redux-thunk": "^2.3.0" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": [ 30 | ">0.2%", 31 | "not dead", 32 | "not ie <= 11", 33 | "not op_mini all" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/dashboard/stylesheets/checkbox.module.sass: -------------------------------------------------------------------------------- 1 | 2 | .outbox 3 | display: flex 4 | align-items: center 5 | margin: .5rem 0 6 | label 7 | margin: 0 8 | display: flex 9 | &:hover 10 | cursor: pointer 11 | input 12 | opacity: 0 13 | display: none 14 | &:checked+label 15 | .checkbox 16 | // background: blue 17 | width: 16px 18 | height: 8px 19 | border-width: 0 0 1px 2px; 20 | opacity: 1; 21 | border-style: solid; 22 | border-color: green 23 | transform: rotateZ(-50deg) scale(1); 24 | animation: check .2s ease-out 25 | @keyframes check 26 | 0% 27 | opacity: 1; 28 | transform: rotateZ(-50deg) scale(0); 29 | 100% 30 | opacity: 1; 31 | transform: rotateZ(-50deg) scale(1.8); 32 | 33 | .checkbox 34 | width: 1rem 35 | height: 1rem 36 | border: 1px solid grey 37 | display: flex 38 | justify-content: center 39 | align-items: center 40 | margin-right: .8rem 41 | 42 | -------------------------------------------------------------------------------- /src/pages/loginsignin/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import LoginSignin from './LoginSignin' 3 | import { 4 | validateExistence, 5 | validateEmail, 6 | validateLength, 7 | validateLowerCase, 8 | validateUpperCase 9 | } from './utils/validation' 10 | 11 | 12 | const INPUT_CONFIG = [ 13 | { 14 | name: "email", 15 | validations: [validateExistence, validateEmail] 16 | }, 17 | { 18 | name: "password", 19 | validations: [validateExistence, validateLength(6, 15), validateLowerCase, validateUpperCase] 20 | } 21 | ] 22 | 23 | export default function Login({ postToken, login_loading, login_error }) { 24 | return ( 25 |
26 | 35 |
36 | ) 37 | } 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/components/header/stylesheets/search.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../../modules/mediaQuery/app' 2 | 3 | .outbox 4 | display: flex 5 | align-items: center 6 | width: 100% 7 | margin: 0 1rem 8 | 9 | .auto 10 | width: max-content 11 | 12 | .btn 13 | margin-left: 1rem 14 | border: 2px solid rgb(94, 114, 100) 15 | border-radius: 5px 16 | background: rgb(65,65,65) 17 | &:hover 18 | background: rgb(94, 114, 100) 19 | button 20 | color: white 21 | button 22 | background: transparent 23 | border: 0 24 | color: white 25 | 26 | @media screen and (max-width: $tablet) 27 | .outbox 28 | display: flex 29 | flex-direction: column 30 | margin: 0 31 | padding: .5rem 0 32 | font-size: .8rem 33 | 34 | .auto 35 | width: 100% 36 | padding: 0 1rem 37 | div 38 | input 39 | width: 100% 40 | 41 | .btn 42 | width: calc( 100% - 2rem) 43 | margin: .5rem 1rem 0 1rem 44 | button 45 | width: 100% 46 | padding: 0 47 | margin: 0 48 | border: none 49 | 50 | -------------------------------------------------------------------------------- /src/modules/mediaQuery/index.js: -------------------------------------------------------------------------------- 1 | export const size = { 2 | mobileS: '320px', 3 | mobileM: '375px', 4 | mobileL: '425px', 5 | tablet: '768px', 6 | laptop: '1024px', 7 | laptopL: '1440px', 8 | desktop: '2560px' 9 | } 10 | const device = { 11 | min: { 12 | mobileS: `(min-width: ${size.mobileS})`, 13 | mobileM: `(min-width: ${size.mobileM})`, 14 | mobileL: `(min-width: ${size.mobileL})`, 15 | tablet: `(min-width: ${size.tablet})`, 16 | laptop: `(min-width: ${size.laptop})`, 17 | laptopL: `(min-width: ${size.laptopL})`, 18 | desktop: `(min-width: ${size.desktop})`, 19 | desktopL: `(min-width: ${size.desktop})` 20 | }, 21 | max: { 22 | mobileS: `(max-width: ${size.mobileS})`, 23 | mobileM: `(max-width: ${size.mobileM})`, 24 | mobileL: `(max-width: ${size.mobileL})`, 25 | tablet: `(max-width: ${size.tablet})`, 26 | laptop: `(max-width: ${size.laptop})`, 27 | laptopL: `(max-width: ${size.laptopL})`, 28 | desktop: `(max-width: ${size.desktop})`, 29 | desktopL: `(max-width: ${size.desktop})` 30 | }, 31 | }; 32 | 33 | export default device -------------------------------------------------------------------------------- /src/components/header/components/UserHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from '../stylesheets/userHeader.module.sass' 3 | import { NavDropdown } from 'react-bootstrap' 4 | import Auth from '../../../modules/Auth' 5 | import jumpTo from '../../../modules/Navigation' 6 | 7 | export default function UserHeader({ user_token }) { 8 | return ( 9 |
10 |
jumpTo('/bag')}> 11 | CART 12 |
13 | {(user_token && Object.keys(user_token).length > 0) 14 | ? 15 |
16 | 17 | 18 | logout 19 | 20 | 21 |
22 | : 23 |
24 |
jumpTo('/login')}> 25 | Login 26 |
27 |
28 | } 29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/productOverview/stylesheets/variants.module.sass: -------------------------------------------------------------------------------- 1 | @import '~bootstrap/scss/_functions' 2 | @import '~bootstrap/scss/_variables' 3 | @import '~bootstrap/scss/mixins/_breakpoints' 4 | @import url('https://fonts.googleapis.com/css?family=Comfortaa'); 5 | 6 | .color_title 7 | color: black 8 | font-weight: bold 9 | font-family: 'Comfortaa', cursive; 10 | margin-bottom: 0.5rem 11 | 12 | .color_name 13 | color: grey 14 | font-family: 'Comfortaa', cursive; 15 | 16 | .sizes 17 | width: 100% 18 | color: black 19 | font-weight: bold 20 | font-family: 'Comfortaa', cursive; 21 | 22 | .size_title 23 | margin-bottom: 0.5rem 24 | 25 | .color_pic 26 | width: 100% 27 | height: 10% 28 | margin-bottom: 1rem 29 | img 30 | width: 10% 31 | margin-right: 0.7rem 32 | 33 | @include media-breakpoint-down(sm) 34 | .color_title 35 | font-size: 0.75rem 36 | margin-bottom: 0.25rem 37 | .color_name 38 | font-size: 0.5rem 39 | .size_title 40 | font-size: 0.75rem 41 | margin-bottom: 0.25rem 42 | .color_pic 43 | margin-bottom: 0.5rem 44 | img 45 | width: 20% 46 | -------------------------------------------------------------------------------- /src/pages/productOverview/components/Variants.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from '../stylesheets/variants.module.sass' 3 | import Sizes from '../components/Sizes' 4 | 5 | 6 | export default function Variants({ color, size, variants, selectedSize, handleClick, clickSize }) { 7 | return ( 8 |
9 |
10 | COLOUR: 11 |
12 |
13 | {color} 14 |
15 |
16 | {variants && variants.map(v => 17 | handleClick(v)} key={v.color} src={v.imagePath} alt="" /> 18 | )} 19 |
20 |
21 |
22 | SIZES: 23 |
24 |
25 | 30 |
31 |
32 |
33 | ) 34 | } 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Zack Yin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/header/components/Menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from '../stylesheets/menu.module.sass' 3 | import DropList from './DropList' 4 | import jumpTo from '../../../modules/Navigation' 5 | export default function Menu({ 6 | departments, 7 | getProductsByCategory, 8 | getAllProducts 9 | }) { 10 | return ( 11 |
12 | {/* lists */} 13 |
14 | {/* departments */} 15 | {departments && departments.map(d => 16 |
19 | getProductsByCategory(c)} 21 | department={d.departmentName} 22 | categories={d.categories.split(',')} 23 | /> 24 |
25 | )} 26 |
27 | {/* all product */} 28 |
{ 30 | getAllProducts() 31 | jumpTo('/dashboard') 32 | }} 33 | > 34 | All Product 35 |
36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/checkout/components/Subtotal.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from '../stylesheets/subtotal.module.sass' 3 | import calcPrice from '../utils/calcPrice' 4 | 5 | export default function Subtotal({ subtotal }) { 6 | return ( 7 |
8 |
9 |
10 |
11 | Subtotal 12 |
13 |
14 | ${subtotal} 15 |
16 |
17 |
18 |
19 | Shipping 20 |
21 |
22 | Free 23 |
24 |
25 |
26 |
27 | Taxes 28 |
29 |
30 | ${calcPrice(subtotal).taxes} 31 |
32 |
33 |
34 |
35 |
36 | Total 37 |
38 |
39 | ${calcPrice(subtotal).total} 40 |
41 |
42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/components/autoComplete/auto.module.sass: -------------------------------------------------------------------------------- 1 | .outbox 2 | display: flex 3 | flex-direction: column 4 | align-items: flex-start 5 | // min-width: max-content 6 | z-index: 3 7 | position: relative 8 | input 9 | // border: 1px solid grey 10 | border: none 11 | outline: none 12 | background: transparent 13 | border-bottom: 1px solid black 14 | box-shadow: none 15 | padding: 0 16 | height: 2rem 17 | caret-color: green 18 | cursor: default 19 | input::placeholder 20 | padding-left: .5rem 21 | color: white !important 22 | .sugges_outbox 23 | border: 1px solid grey 24 | min-width: min-content 25 | background: white 26 | position: absolute 27 | top: 2rem 28 | .sugges_box 29 | 30 | .sugges_category 31 | padding-left: 1rem 32 | font-size: 1.1rem 33 | color: white 34 | background: rgb(141, 232, 219) 35 | .sugges_arr 36 | display: flex 37 | flex-direction: column 38 | .sugges_value 39 | font-size: 1rem 40 | color: grey 41 | padding: 0 2rem 42 | white-space: nowrap 43 | width: min-content 44 | min-width: 100% 45 | &:hover 46 | background: rgb(141, 232, 219) 47 | width: 100% 48 | cursor: pointer 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/pages/checkoutSuccess/stylesheet/checkoutSuccess.module.sass: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Comfortaa'); 2 | 3 | 4 | .outbox 5 | width: 100vw 6 | min-height: 100vh 7 | background: rgb(244, 244, 244) 8 | 9 | .box 10 | width: 100% 11 | padding-top: 12vh 12 | display: flex 13 | flex-direction: column 14 | align-items: center 15 | 16 | .checkMark 17 | display: flex 18 | flex-direction: column 19 | align-items: center 20 | .pic 21 | width: 100px 22 | height: 100px 23 | img 24 | width: 100% 25 | height: 100% 26 | .text 27 | font-size: 2rem 28 | font-family: 'Comfortaa', cursive; 29 | 30 | 31 | .content 32 | width: 100% 33 | padding: 3% 34 | background: white 35 | 36 | .order 37 | display: flex 38 | .num 39 | color: blue 40 | font-weight: 500 41 | margin: 1rem 42 | .title 43 | font-weight: 700 44 | margin: 1rem 0 45 | 46 | .btn 47 | display: flex 48 | justify-content: flex-end 49 | font-family: 'Comfortaa', cursive; 50 | button 51 | background: white 52 | border: 2px groove rgba(0,0,255,0.5) 53 | border-radius: 5px 54 | &:hover 55 | background: rgba(0,0,255,0.3) 56 | color: white -------------------------------------------------------------------------------- /src/pages/loginsignin/components/Base.js: -------------------------------------------------------------------------------- 1 | import styles from '../stylesheets/base.module.sass' 2 | import React from 'react' 3 | import FormInput from './FormInput' 4 | import Button from './Button' 5 | import Footer from './footer' 6 | 7 | export default function Base({ 8 | title, 9 | inputs, 10 | onInputBlur, 11 | onInputFocus, 12 | onInputChange, 13 | onSubmit, 14 | errorMsg, 15 | button_title, 16 | footer_content 17 | }) { 18 | return ( 19 |
20 |
Zack Market
21 |
{title}
22 |
23 | { 24 | inputs.map(({ name, validations }) => 25 | 34 | ) 35 | } 36 |
39 | ) 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/pages/loginsignin/Signin.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import LoginSignin from './LoginSignin' 3 | import { 4 | validateExistence, 5 | validateEmail, 6 | validateLength, 7 | validateLowerCase, 8 | validateUpperCase 9 | } from './utils/validation' 10 | 11 | 12 | const INPUT_CONFIG = [ 13 | { 14 | name: "fullname", 15 | // validations: [validateLength()] 16 | }, 17 | { 18 | name: "email", 19 | validations: [validateExistence, validateEmail] 20 | }, 21 | { 22 | name: "password", 23 | validations: [validateExistence, validateLength(6, 15), validateLowerCase, validateUpperCase] 24 | }, 25 | { 26 | name: "verifyPassword", 27 | validations: [validateExistence, validateLength(6, 15), validateLowerCase, validateUpperCase] 28 | }, 29 | ] 30 | 31 | 32 | export default function Signin({signin,signin_loading,signin_error}) { 33 | return ( 34 |
35 | 44 |
45 | ) 46 | } 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/pages/shoppingBag/stylesheets/shoppingBag.module.sass: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Comfortaa'); 2 | @import '../../../modules/mediaQuery/app' 3 | 4 | 5 | .outbox 6 | min-width: max-content 7 | min-width: 386px 8 | min-height: 100vh 9 | display: flex 10 | justify-content: center 11 | background: rgb(244, 244, 244) 12 | font-size: 1rem 13 | 14 | .box 15 | width: 100% 16 | margin-top: 12vh 17 | display: flex 18 | justify-content: center 19 | align-items: center 20 | 21 | 22 | .content 23 | padding: 3% 24 | margin: 4em 25 | background: white 26 | min-width: max-content 27 | justify-content: center 28 | align-items: center 29 | 30 | 31 | .title 32 | color: blue 33 | font-size: 2rem 34 | font-family: 'Comfortaa', cursive; 35 | 36 | 37 | .table 38 | width: max-content 39 | 40 | .process_box 41 | width: 100% 42 | display: flex 43 | flex-direction: column 44 | align-items: flex-end 45 | 46 | .total 47 | font-size: 20px 48 | font-weight: bold 49 | 50 | 51 | 52 | // @include media-breakpoint-down(md) 53 | // .box 54 | // max-width: 70% 55 | // @include media-breakpoint-down(sm) 56 | // .box 57 | // max-width: 90% 58 | // .title 59 | // font-size: 1.5rem 60 | -------------------------------------------------------------------------------- /src/redux/reducer/tokenReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | POST_TOKEN_BEGIN, 3 | POST_TOKEN_SUCCESS, 4 | POST_TOKEN_FAIL, 5 | INSERT_TOKEN_SUCCESS, 6 | INSERT_TOKEN_FAIL 7 | } from '../action/tokenAction' 8 | 9 | const initialState = { 10 | user_token: {}, 11 | token_loading: false, 12 | error: {}, 13 | insert_token_error: false 14 | } 15 | 16 | export default (state = initialState, action) => { 17 | switch (action.type) { 18 | case POST_TOKEN_BEGIN: 19 | return { 20 | ...state, 21 | token_loading: true, 22 | error: {} 23 | } 24 | case POST_TOKEN_SUCCESS: 25 | return { 26 | ...state, 27 | user_token: action.payload.data.user_token, 28 | token_loading: false 29 | } 30 | case POST_TOKEN_FAIL: 31 | return { 32 | ...state, 33 | token_loading: false, 34 | error: action.payload.error.response.data 35 | } 36 | case INSERT_TOKEN_SUCCESS: 37 | return { 38 | ...state, 39 | user_token: action.payload, 40 | insert_token_error: false 41 | } 42 | case INSERT_TOKEN_FAIL: 43 | return { 44 | ...state, 45 | insert_token_error: true 46 | } 47 | default: 48 | return state 49 | } 50 | } -------------------------------------------------------------------------------- /src/pages/loginsignin/utils/validation.js: -------------------------------------------------------------------------------- 1 | class Validation { 2 | constructor(assertion, rule, errMsg) { 3 | this.assertion = assertion 4 | this.rule = rule 5 | this.errMsg = errMsg 6 | } 7 | check(text) { 8 | return this.rule(text) 9 | } 10 | errMsg() { 11 | return this.errMsg 12 | } 13 | } 14 | 15 | export const validateExistence = new Validation( 16 | 'input should have value', 17 | text => /\S/.test(text), 18 | 'Required' 19 | ) 20 | export const validateEmail = new Validation( 21 | 'input should be an email', 22 | text => !!text.match(/[\w-]+@([\w-]+\.)+[\w-]+/i), 23 | 'invalid email' 24 | ) 25 | export const validateLength = (min_len, max_len) => { 26 | const regex = new RegExp(`(?=.{${min_len},${max_len}})`, 'g') 27 | return new Validation( 28 | 'input should within certain length', 29 | text => !!text.match(regex), 30 | `password has to be ${min_len}-${max_len} letter` 31 | ) 32 | } 33 | export const validateLowerCase = new Validation( 34 | 'input should have an uppercase', 35 | text => !!text.match(/(?=.*[A-Z])/g), 36 | 'at least one upper case' 37 | ) 38 | export const validateUpperCase = new Validation( 39 | 'input should have lowercase', 40 | text => !!text.match(/(?=.*[a-z])/g), 41 | 'at least one lower case' 42 | ) -------------------------------------------------------------------------------- /src/pages/loginsignin/components/FormInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from '../stylesheets/formInput.module.sass' 3 | import capitalizeString from '../utils/capitalizeString' 4 | 5 | 6 | export default function FormInput({ 7 | name, 8 | validations, 9 | errorMessage, 10 | onBlur, 11 | onFocus, 12 | }) { 13 | return ( 14 |
15 |
16 | 17 |
18 |
19 | -1 ? 'password' : 'text'} 21 | name={name} 22 | placeholder={name} 23 | onBlur={(e) => onBlur(e, validate(validations, e.target.value))} 24 | onFocus={onFocus} 25 | /> 26 |
27 |
28 | {errorMessage} 29 |
30 |
31 | ) 32 | } 33 | 34 | const validate = (validations, val) => { 35 | if (validations) { 36 | for (const validation of validations) { 37 | if (!validation.check(val)) { 38 | return { 39 | isValid: false, 40 | errorMsg: validation.errMsg 41 | } 42 | } 43 | } 44 | } 45 | return { isValid: true } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/redux/reducer/checkoutReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_CHECKOUT_BEGIN, 3 | GET_CHECKOUT_SUCCESS, 4 | GET_CHECKOUT_FAIL, 5 | GET_PAYMENT_BEGIN, 6 | GET_PAYMENT_SUCCESS, 7 | GET_PAYMENT_FAIL 8 | } from '../action/checkoutAction' 9 | 10 | const initialState = { 11 | loading: false, 12 | approval_url: null, 13 | payment: null, 14 | error: null, 15 | } 16 | 17 | export default (state = initialState, action) => { 18 | switch (action.type) { 19 | case GET_CHECKOUT_BEGIN: 20 | return { 21 | ...state, 22 | loading: true, 23 | error: null 24 | } 25 | case GET_CHECKOUT_SUCCESS: 26 | return { 27 | loading: false, 28 | approval_url: action.payload.data 29 | } 30 | case GET_CHECKOUT_FAIL: 31 | return { 32 | loading: false, 33 | error: action.payload.error.response.data 34 | } 35 | case GET_PAYMENT_BEGIN: 36 | return { 37 | ...state, 38 | loading: true, 39 | error: null 40 | } 41 | case GET_PAYMENT_SUCCESS: 42 | return { 43 | loading: false, 44 | payment: action.payload.data.payment 45 | } 46 | case GET_PAYMENT_FAIL: 47 | return { 48 | loading: false, 49 | error: action.payload.error.response.data 50 | } 51 | default: 52 | return state 53 | } 54 | } -------------------------------------------------------------------------------- /src/redux/action/tokenAction.js: -------------------------------------------------------------------------------- 1 | import { login } from '../../modules/serverCall' 2 | 3 | export const postToken = (email, password) => dispatch => { 4 | dispatch({ 5 | type: POST_TOKEN_BEGIN 6 | }) 7 | return login(email, password) 8 | .then(res => { 9 | dispatch({ 10 | type: POST_TOKEN_SUCCESS, 11 | payload: res 12 | }) 13 | // console.log('tokenAction res'); 14 | // console.log(res); 15 | return res 16 | }) 17 | .catch(error => { 18 | dispatch({ 19 | type: POST_TOKEN_FAIL, 20 | payload: { error } 21 | }) 22 | // console.log('tokenAction error'); 23 | // console.log(error.response); 24 | throw error 25 | }) 26 | } 27 | 28 | export const insertToken = () => dispatch => { 29 | let token 30 | if (localStorage.getItem('auth')) { 31 | token = JSON.parse(localStorage.getItem('auth')) 32 | dispatch({ 33 | type: INSERT_TOKEN_SUCCESS, 34 | payload: token 35 | }) 36 | } else { 37 | dispatch({ 38 | type: INSERT_TOKEN_FAIL 39 | }) 40 | } 41 | } 42 | 43 | export const POST_TOKEN_BEGIN = 'POST_TOKEN_BEGIN' 44 | export const POST_TOKEN_SUCCESS = 'POST_TOKEN_SUCCESS' 45 | export const POST_TOKEN_FAIL = 'POST_TOKEN_FAIL' 46 | export const INSERT_TOKEN_SUCCESS = 'INSERT_TOKEN_SUCCESS' 47 | export const INSERT_TOKEN_FAIL = 'INSERT_TOKEN_FAIL' 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/redux/reducer/cartReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | POST_CART_BEGIN, 3 | POST_CART_SUCCESS, 4 | POST_CART_FAIL, 5 | GET_CART_BY_USERID_BEGIN, 6 | GET_CART_BY_USERID_SUCCESS, 7 | GET_CART_BY_USERID_FAIL 8 | } from '../action/cartAction' 9 | 10 | const initialState = { 11 | cart: {}, 12 | loading: false, 13 | error: {}, 14 | } 15 | 16 | export default (state = initialState, action) => { 17 | switch (action.type) { 18 | case POST_CART_BEGIN: 19 | return { 20 | ...state, 21 | loading: true, 22 | error: {} 23 | } 24 | case POST_CART_SUCCESS: 25 | return { 26 | ...state, 27 | cart: action.payload.data.cart, 28 | loading: false 29 | } 30 | case POST_CART_FAIL: 31 | return { 32 | ...state, 33 | loading: false, 34 | error: action.payload.error.response.data 35 | } 36 | case GET_CART_BY_USERID_BEGIN: 37 | return { 38 | ...state, 39 | loading: true, 40 | error: {} 41 | } 42 | case GET_CART_BY_USERID_SUCCESS: 43 | return { 44 | ...state, 45 | cart: action.payload.data.cart, 46 | loading: false 47 | } 48 | case GET_CART_BY_USERID_FAIL: 49 | return { 50 | ...state, 51 | loading: false, 52 | error: action.payload.error.response.data 53 | } 54 | default: 55 | return state 56 | } 57 | } -------------------------------------------------------------------------------- /src/pages/shoppingBag/ShoppingBag.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from '../../components/header/headerContainer' 3 | import styles from './stylesheets/shoppingBag.module.sass' 4 | import { Button } from 'react-bootstrap' 5 | import Table from './components/Table' 6 | import jumpTo from '../../modules/Navigation' 7 | 8 | 9 | export default function ShoppingBag(props) { 10 | const { totalPrice, items } = props.cart 11 | const { postCart } = props 12 | return ( 13 |
14 |
15 |
16 |
17 |
18 | Shopping Bag 19 |
20 |
21 | postCart(pid, increase, decrease)} 24 | /> 25 | 26 |
27 |
28 | Total: ${totalPrice} 29 |
30 |
31 | 34 |
35 |
36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/checkout/stylesheets/checkout.module.sass: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Comfortaa'); 2 | @import '../../../modules/mediaQuery/app' 3 | 4 | 5 | .outbox 6 | max-width: 100vw 7 | min-width: 320px 8 | min-height: 100vh 9 | display: flex 10 | justify-content: center 11 | background: rgb(244, 244, 244) 12 | font-size: 1rem 13 | 14 | .box 15 | margin: 12vh 0 16 | display: flex 17 | justify-content: center 18 | 19 | .content 20 | width: 60% 21 | background: white 22 | display: flex 23 | flex-direction: column 24 | padding: 2em 25 | margin: 2em 26 | 27 | .title 28 | color: blue 29 | font-size: 2em 30 | font-family: 'Comfortaa', cursive; 31 | .sub_title 32 | color: black 33 | font-size: .5em 34 | display: block 35 | margin: 1% 0 36 | 37 | .table 38 | border-top: 1px solid black 39 | border-bottom: 1px solid black 40 | padding: 2% 41 | .prices 42 | padding: 2% 0 43 | border-bottom: 1px solid black 44 | .btn 45 | display: flex 46 | justify-content: flex-end 47 | margin: 2% 0 48 | button 49 | background: white 50 | border: 2px groove rgba(0,0,255,0.5) 51 | border-radius: 5px 52 | &:hover 53 | background: rgba(0,0,255,0.3) 54 | a 55 | color: white 56 | a 57 | text-decoration: none 58 | color: black 59 | font-family: 'Comfortaa', cursive; 60 | 61 | 62 | @media screen and (max-width: $tablet) 63 | .outbox 64 | font-size: .7rem 65 | 66 | .content 67 | width: 80% 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/assets/images/loading-bars.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/images/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/redux/action/cartAction.js: -------------------------------------------------------------------------------- 1 | import serverCall from '../../modules/serverCall' 2 | import Auth from '../../modules/Auth'; 3 | 4 | 5 | export const getCartByUserId = () => dispatch => { 6 | let userId = Auth.getUserId() 7 | dispatch({ 8 | type: GET_CART_BY_USERID_BEGIN 9 | }) 10 | return serverCall({ 11 | method: 'GET', 12 | url: `users/${userId}/cart` 13 | }) 14 | .then(res => { 15 | dispatch({ 16 | type: GET_CART_BY_USERID_SUCCESS, 17 | payload: res 18 | }) 19 | return res 20 | }) 21 | .catch(error => { 22 | dispatch({ 23 | type: GET_CART_BY_USERID_FAIL, 24 | payload: { error } 25 | }) 26 | return error 27 | }) 28 | } 29 | 30 | export const postCart = (productId, increase, decrease) => (dispatch) => { 31 | let userId = Auth.getUserId() 32 | dispatch({ 33 | type: POST_CART_BEGIN 34 | }) 35 | return serverCall({ 36 | method: 'POST', 37 | url: `users/${userId}/cart`, 38 | data: { 39 | userId, 40 | productId, 41 | increase, 42 | decrease 43 | } 44 | }) 45 | .then(res => { 46 | dispatch({ 47 | type: POST_CART_SUCCESS, 48 | payload: res 49 | }) 50 | return res 51 | }) 52 | .catch(error => { 53 | dispatch({ 54 | type: POST_CART_FAIL, 55 | payload: { error } 56 | }) 57 | return error 58 | }) 59 | } 60 | 61 | export const POST_CART_BEGIN = 'POST_CART_BEGIN' 62 | export const POST_CART_SUCCESS = 'POST_CART_SUCCESS' 63 | export const POST_CART_FAIL = 'POST_CART_FAIL' 64 | 65 | export const GET_CART_BY_USERID_BEGIN = 'GET_CART_BY_USERID_BEGIN' 66 | export const GET_CART_BY_USERID_SUCCESS = 'GET_CART_BY_USERID_SUCCESS' 67 | export const GET_CART_BY_USERID_FAIL = 'GET_CART_BY_USERID_FAIL' -------------------------------------------------------------------------------- /src/pages/dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import HeaderContainer from '../../components/header/headerContainer' 3 | import Product from './components/Product' 4 | import LoadingAnimation from '../../components/loadingAnimation' 5 | import Filter from './components/Filter' 6 | import styles from './stylesheets/dashboard.module.sass' 7 | 8 | 9 | export default class Dashboard extends Component { 10 | constructor(props) { 11 | super(props); 12 | } 13 | componentDidMount() { 14 | if (!this.props.products) { 15 | this.props.getAllProducts() 16 | } 17 | } 18 | render() { 19 | const { products, applyFilters } = this.props 20 | return ( 21 |
22 | {/* Header */} 23 | 24 |
25 | {/* loading animation */} 26 | {this.props.loading && 27 | 28 | } 29 | {/* filter */} 30 |
31 | 34 |
35 | {/* products */} 36 |
37 | {products && products.map(p => 38 |
this.props.history.push(`/product-overview/${p._id}`)}> 42 | 48 |
49 | )} 50 |
51 |
52 |
53 | ) 54 | } 55 | } 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/modules/serverCall/index.js: -------------------------------------------------------------------------------- 1 | import Auth from '../Auth' 2 | import jumpTo from '../Navigation' 3 | import axios from 'axios' 4 | import qs from 'qs' 5 | import paypalConfig from '../../configs/paypalConfig' 6 | 7 | const URL = 'https://zack-ecommerce-nodejs.herokuapp.com' 8 | // const URL = 'http://localhost:4000' 9 | 10 | const serverCall = (config) => { 11 | //header authorization 12 | if (Auth.user_token) { 13 | const token = Auth.getToken() 14 | config.headers = { 15 | "authorization": token 16 | } 17 | } 18 | //interceptors handle network error 19 | axios.interceptors.response.use( 20 | (response) => { 21 | return response; 22 | }, 23 | function (error) { 24 | if (!error.response) { 25 | error.response = { 26 | data: 'net work error', 27 | status: 500 28 | } 29 | } 30 | if(error.response.status===401){ 31 | Auth.logout() 32 | jumpTo('/login') 33 | throw error 34 | } 35 | return Promise.reject(error); 36 | }); 37 | config.baseURL = URL 38 | return axios(config) 39 | } 40 | export default serverCall 41 | 42 | export const login = (email, password) => { 43 | const body = 44 | { 45 | "credential": { 46 | "email": email, 47 | "password": password 48 | } 49 | } 50 | return serverCall({ 51 | method: 'POST', 52 | url: '/users/login', 53 | data: body 54 | }) 55 | .then(res => { 56 | Auth.setUserToken(res.data.user_token) 57 | return res 58 | }) 59 | } 60 | 61 | export const getPaypalToken = () => { 62 | return axios({ 63 | method: 'POST', 64 | url: 'https://api.sandbox.paypal.com/v1/oauth2/token', 65 | headers: { 'content-type': 'application/x-www-form-urlencoded' }, 66 | auth: { 67 | username: paypalConfig.username, 68 | password: paypalConfig.password 69 | }, 70 | data: qs.stringify({ "grant_type": "client_credentials" }) 71 | }) 72 | } -------------------------------------------------------------------------------- /src/redux/action/checkoutAction.js: -------------------------------------------------------------------------------- 1 | import serverCall, { getPaypalToken } from '../../modules/serverCall' 2 | 3 | export const getCheckoutUrl = (cartId) => dispatch => { 4 | dispatch({ 5 | type: GET_CHECKOUT_BEGIN, 6 | }) 7 | return serverCall({ 8 | method: 'GET', 9 | url: `/checkout/${cartId}` 10 | }) 11 | .then(res => { 12 | dispatch({ 13 | type: GET_CHECKOUT_SUCCESS, 14 | payload: res 15 | }) 16 | return res 17 | }) 18 | .catch(error => { 19 | dispatch({ 20 | type: GET_CHECKOUT_FAIL, 21 | payload: { error } 22 | }) 23 | return error 24 | }) 25 | } 26 | 27 | export const getPayment = (paymentId, PayerID) => dispatch => { 28 | dispatch({ 29 | type: GET_PAYMENT_BEGIN, 30 | }) 31 | return getPaypalToken() 32 | .then(response => { 33 | return serverCall({ 34 | method: 'GET', 35 | url: `/payment/success?paymentId=${paymentId}&PayerID=${PayerID}`, 36 | headers: { 'Authorization': `Bearer ${response.data.access_token}` } 37 | }) 38 | .then(res => { 39 | dispatch({ 40 | type: GET_PAYMENT_SUCCESS, 41 | payload: res 42 | }) 43 | return res 44 | }) 45 | .catch(error => { 46 | dispatch({ 47 | type: GET_PAYMENT_FAIL, 48 | payload: { error } 49 | }) 50 | return error 51 | }) 52 | }) 53 | .catch(error => { 54 | dispatch({ 55 | type: GET_PAYMENT_FAIL, 56 | payload: { error } 57 | }) 58 | return error 59 | }) 60 | } 61 | 62 | 63 | export const GET_PAYMENT_BEGIN = 'GET_PAYMENT_BEGIN' 64 | export const GET_PAYMENT_SUCCESS = 'GET_PAYMENT_SUCCESS' 65 | export const GET_PAYMENT_FAIL = 'GET_PAYMENT_FAIL' 66 | 67 | export const GET_CHECKOUT_BEGIN = 'GET_CHECKOUT_BEGIN' 68 | export const GET_CHECKOUT_SUCCESS = 'GET_CHECKOUT_SUCCESS' 69 | export const GET_CHECKOUT_FAIL = 'GET_CHECKOUT_FAIL' -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | 31 | React App 32 | 33 | 34 | 35 |
36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/pages/productOverview/stylesheets/productOverview.module.sass: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Comfortaa'); 2 | @import '~bootstrap/scss/_functions' 3 | @import '~bootstrap/scss/_variables' 4 | @import '~bootstrap/scss/mixins/_breakpoints' 5 | 6 | .outbox 7 | min-width: 320px 8 | min-height: 568px 9 | width: 100vw 10 | height: 100vh 11 | display: flex 12 | flex-direction: column 13 | justify-content: center 14 | align-items: center 15 | background: rgb(244, 244, 244) 16 | 17 | .content_box 18 | width: 70% 19 | height: 80% 20 | display: flex 21 | align-items: center 22 | margin-top: 12vh 23 | 24 | .content 25 | width: 100% 26 | height: 100% 27 | display: flex 28 | 29 | .image 30 | max-width: 50% 31 | // max-height: 100% 32 | overflow: hidden 33 | img 34 | width: 100% 35 | object-fit: cover 36 | 37 | // height: 100% 38 | 39 | .context_outbox 40 | width: 50% 41 | max-height: 100% 42 | margin-left: 3% 43 | 44 | .context 45 | display: flex 46 | flex-direction: column 47 | justify-content: flex-start 48 | background: white 49 | padding: 10px 50 | 51 | .title 52 | color: blue 53 | font-size: 1.5rem 54 | font-family: 'Comfortaa', cursive; 55 | 56 | .description 57 | color: grey 58 | font-size: 1rem 59 | 60 | .price 61 | color: blue 62 | font-size: 1.5rem 63 | font-family: 'Comfortaa', cursive; 64 | 65 | .border 66 | border-bottom: 2px dotted grey 67 | margin: 20px -10px 68 | 69 | .btns 70 | width: 100% 71 | margin: 1rem 0 72 | display: flex 73 | justify-content: space-between 74 | button 75 | width: 45% 76 | 77 | @include media-breakpoint-down(lg) 78 | .content_box 79 | width: 90% 80 | 81 | @include media-breakpoint-down(sm) 82 | .title 83 | font-size: 1rem 84 | .description 85 | font-size: 0.7rem 86 | .price 87 | font-size: 1rem 88 | .border 89 | margin: 0.5rem -0.5rem 90 | .btns 91 | .btn 92 | font-size: 0.75rem 93 | width: 47% 94 | padding: 0.25rem 0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full stack ecommerce online store application 2 | 3 | #### back-end code [click here](https://github.com/levelopers/Ecommerce-Nodejs) 4 | 5 | ## General Info 6 | 7 | front-end: Reactjs, Redux, Axios, Sass, react-bootstrap 8 | 9 | back-end: Node.js, Express, Restful API, paypal-sdk, mongodb, mongoose, jwt(jsonwebtoken), heroku, firebase, swaggerHub 10 | 11 | ## snapshots 12 | 13 | 14 |
15 | dashboard 16 |

17 | 18 | ![dashboard_large](https://user-images.githubusercontent.com/38830527/57897997-477ce000-7825-11e9-84f2-dcc66c7a378b.png) 19 | - - - - - 20 | side menu | dashboard 21 | :-------------------------:|:-------------------------: 22 | ![side_menu_sm](https://user-images.githubusercontent.com/38830527/57898258-3e404300-7826-11e9-97dc-1475eaf95c70.png) | ![dashboard_sm](https://user-images.githubusercontent.com/38830527/57898213-0f29d180-7826-11e9-97cd-a19323b5d5cd.png) 23 |

24 |
25 | 26 |
27 | product-overview 28 |

29 | 30 | ![product_overview_large](https://user-images.githubusercontent.com/38830527/57897999-477ce000-7825-11e9-989e-4d95938b6578.PNG) 31 |

32 |
33 | 34 |
35 | checkout 36 |

37 | 38 | ![checkout_large](https://user-images.githubusercontent.com/38830527/57897994-46e44980-7825-11e9-86ec-5c086675c98d.PNG) 39 | 40 |

41 |
42 | 43 | 44 |
45 | checkout confirm 46 |

47 | 48 | ![checkout_confirm_large](https://user-images.githubusercontent.com/38830527/57897993-46e44980-7825-11e9-9ecf-6de74dd69eb2.PNG) 49 | 50 |

51 |
52 | 53 | 54 |
55 | checkout success 56 |

57 | 58 | ![checkout_success_large](https://user-images.githubusercontent.com/38830527/57897996-46e44980-7825-11e9-8247-c0d97cfc39e9.PNG) 59 | 60 |

61 |
62 | 63 | 64 |
65 | login 66 |

67 | 68 | ![login_large](https://user-images.githubusercontent.com/38830527/57897998-477ce000-7825-11e9-84d8-16d59b65edb4.PNG) 69 | 70 |

71 |
72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/pages/checkout/Checkout.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styles from './stylesheets/checkout.module.sass' 3 | import CheckoutTable from './components/CheckoutTable' 4 | import Subtotal from './components/Subtotal' 5 | import Header from '../../components/header/headerContainer' 6 | export default class Checkout extends Component { 7 | constructor(props) { 8 | super(props) 9 | 10 | } 11 | componentDidMount() { 12 | if (Object.keys(this.props.cart).length < 1) { 13 | this.props.getCartByUserId() 14 | } else { 15 | this.props.getCheckoutUrl(this.props.cart._id) 16 | } 17 | } 18 | componentDidUpdate() { 19 | if (!this.props.url && Object.keys(this.props.cart).length > 1) { 20 | this.props.getCheckoutUrl(this.props.cart._id) 21 | } 22 | } 23 | render() { 24 | return ( 25 |
26 |
27 |
28 |
29 | {/* title */} 30 |
31 | Checkout 32 |
33 | Hi {this.props.name} Please review your items and press the confirm checkout button. You will enter your address information while your paying on PayPal 34 |
35 |
36 | {/* table */} 37 | {Object.keys(this.props.cart).length > 0 && 38 |
39 |
40 | 43 |
44 | {/* prices */} 45 |
46 | 49 |
50 |
51 | } 52 | {/* button */} 53 |
54 | 55 |
56 |
57 |
58 |
59 | ) 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; 3 | import { connect } from 'react-redux' 4 | import { registerNav } from './modules/Navigation' 5 | import { insertToken } from './redux/action/tokenAction' 6 | import LoginContainer from './pages/loginsignin/LoginContainer' 7 | import SigninContainer from './pages/loginsignin/SigninContainer' 8 | import DashboardContainer from './pages/dashboard/DashboardContainer' 9 | import ProductOverview from './pages/productOverview/ProductOverviewContainer' 10 | import ShoppingBagContainer from './pages/shoppingBag/ShoppingBagContainer' 11 | import CheckoutContainer from './pages/checkout/checkoutContainer' 12 | import CheckoutSuccessContainer from './pages/checkoutSuccess/CheckoutSuccessContainer' 13 | import CheckoutCancel from './pages/checkoutCancel/CheckoutCancel' 14 | 15 | class App extends Component { 16 | componentDidMount() { 17 | this.props.insertToken() 18 | } 19 | render() { 20 | return ( 21 |
22 | 23 | 24 | 25 | 26 | , 27 | {this.props.token && [ 28 | , 29 | , 30 | , 31 | , 32 | ]} 33 | , 34 | 35 | 36 | 37 | 38 |
39 | ); 40 | } 41 | } 42 | const mapStoreToProps = state => ({ 43 | token: state.token.user_token 44 | }) 45 | const mapDispatchToProps = { 46 | insertToken 47 | } 48 | export default connect(mapStoreToProps, mapDispatchToProps)(App); 49 | -------------------------------------------------------------------------------- /src/components/autoComplete/AutoComplete.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styles from './auto.module.sass' 3 | export default class AutoComplete extends Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | isFocus: false, 8 | val: '' 9 | } 10 | } 11 | handleChange = (e) => { 12 | const input = e.target.value 13 | this.props.filter(input) 14 | if(this.props.onChange){ 15 | this.props.onChange(input) 16 | } 17 | this.setState({ 18 | val: input 19 | }) 20 | } 21 | handleFocus = () => { 22 | this.setState({ 23 | isFocus: true 24 | }) 25 | } 26 | handleBlur = () => { 27 | let blurTimer=null 28 | blurTimer=setTimeout(() => { 29 | this.setState({ 30 | isFocus: false 31 | },()=>clearTimeout(blurTimer)) 32 | }, 100); 33 | } 34 | handleClick = (v) => { 35 | if(this.props.suggest_value){ 36 | this.props.suggest_value(v) 37 | } 38 | this.setState({ 39 | val: v 40 | }) 41 | } 42 | render() { 43 | const r = this.props.filter_result 44 | return ( 45 |
46 | 54 |
55 | { 56 | this.state.isFocus && 57 | !this.props.error && !this.props.loading && 58 | Object.keys(r || {}).map(f => 59 |
60 |
61 | {f} 62 |
63 |
64 | {r[f].map(s => 65 |
this.handleClick(s)} 69 | > 70 | {s} 71 |
72 | )} 73 |
74 |
75 | ) 76 | } 77 |
78 |
79 | ) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/pages/shoppingBag/components/Table.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Table } from 'react-bootstrap' 3 | import styles from '../stylesheets/table.module.sass' 4 | import { Button } from 'react-bootstrap' 5 | 6 | export default function BagTable({ items, handleClick }) { 7 | return ( 8 |
9 |
10 | 11 | 12 | {/* */} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {Object.keys(items).map(id => 21 | // table row 22 | 23 | {/* pic */} 24 | 29 | {/* title */} 30 | 43 | {/* qty */} 44 | 63 | {/* price */} 64 | 65 | 66 | )} 67 | 68 |
idPhotosTitleQtyPrice
25 |
26 | 27 |
28 |
31 |
32 |
33 | {items[id].item.title} 34 |
35 |
36 | id: 37 |
38 |
39 | {items[id].item._id} 40 |
41 |
42 |
45 |
46 | 53 | {items[id].qty} 54 | 61 |
62 |
{items[id].price}
69 |
70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /src/pages/checkoutSuccess/CheckoutSuccess.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import mapSearchURL from './utils/mapSearchURL' 3 | import jumpTo from '../../modules/Navigation' 4 | import Header from '../../components/header/headerContainer' 5 | import checkMark from '../../assets/images/checkmark.svg' 6 | import styles from './stylesheet/checkoutSuccess.module.sass' 7 | export default class CheckoutSuccess extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | } 12 | componentDidMount() { 13 | const UrlQuery = this.props.location.search 14 | const queryPair = mapSearchURL(UrlQuery) 15 | if (queryPair.has("paymentId") && queryPair.has("PayerID")) { 16 | this.props.getPayment(queryPair.get("paymentId"), queryPair.get("PayerID")) 17 | } 18 | } 19 | 20 | render() { 21 | const payment = this.props.payment 22 | return ( 23 |
24 |
25 | {payment && 26 |
27 |
28 |
29 | 30 |
31 |
32 | Thank you for your pruchase! 33 |
34 |
35 |
36 |
37 |
38 |
39 | Your order number is:
40 |
41 | {payment.cart} 42 |
43 |
44 |
45 |
46 | Billing & Shipping information: 47 |
48 | {Object.keys(payment.payer.payer_info.shipping_address).map(p => 49 |
50 | {p}: {payment.payer.payer_info.shipping_address[p]} 51 |
52 | )} 53 |
54 |
55 |
56 | 57 |
58 |
59 |
60 | } 61 |
62 | ) 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/pages/dashboard/utils/generateFilterString.js: -------------------------------------------------------------------------------- 1 | /** 2 | * { 3 | * order:[], 4 | * price:[], 5 | * department:[] 6 | * } 7 | */ 8 | export default (state) => { 9 | let result = '' 10 | let order = '' 11 | let department = '' 12 | let range = '' 13 | for (let name in state) { 14 | // order filter 15 | if (name === 'order') { 16 | let temp = '' 17 | for (const v of state[name]) { 18 | temp = v 19 | } 20 | if (temp.toUpperCase() === 'ASCENDING') { 21 | order = 'order=price' 22 | } 23 | if (temp.toUpperCase() === 'DESCENDING') { 24 | order = 'order=-price' 25 | } 26 | } 27 | // department filter 28 | if (name === 'department') { 29 | let _base_str = '' 30 | if (state[name].length > 1) { 31 | _base_str = '&department=' 32 | } else { 33 | _base_str = 'department=' 34 | } 35 | for (let d of state[name]) { 36 | department += _base_str + d.charAt(0).concat(d.slice(1).toLowerCase()) 37 | } 38 | } 39 | // price filter 40 | if (name === 'price') { 41 | let _price_str_arr = [] 42 | for (let p of state[name]) { 43 | if (p.match(/less/i)) { 44 | p = p.replace(/[\D]+/i, '0 - ') 45 | } 46 | if (p.match(/greater/i)) { 47 | p = p.replace(/[\D]+/i, '').concat(' - 999') 48 | } 49 | _price_str_arr = _price_str_arr.concat(p.match(/[\d]+/g)) 50 | } 51 | // sort the array 52 | let sorted_matched_arr = _price_str_arr.sort(function (a, b) { return a - b }) 53 | // remove duplicates 54 | for (let index = 0; index < sorted_matched_arr.length; index++) { 55 | if (sorted_matched_arr[index] === sorted_matched_arr[index + 1]) { 56 | sorted_matched_arr.splice(index, 2) 57 | index-- 58 | } 59 | } 60 | // generate price range string 61 | for (let index = 0; index < sorted_matched_arr.length; index++) { 62 | if (index % 2 === 0) { 63 | if (range) { 64 | range += `&range=${sorted_matched_arr[index]}-${sorted_matched_arr[index + 1]}` 65 | } else { 66 | range += `range=${sorted_matched_arr[index]}-${sorted_matched_arr[index + 1]}` 67 | } 68 | } 69 | } 70 | } 71 | } 72 | if (range) { 73 | result += range 74 | } 75 | if (order) { 76 | if (result) { 77 | order = `&${order}` 78 | } 79 | result += order 80 | } 81 | if (department) { 82 | if (result) { 83 | department = `&${department}` 84 | } 85 | result += department 86 | } 87 | // console.log(result); 88 | return result 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/pages/dashboard/stylesheets/filter_md.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../../modules/mediaQuery/app' 2 | 3 | @media screen and (max-width: $tablet) 4 | .outbox 5 | width: 100vw 6 | 7 | .panels 8 | margin-left: 1rem 9 | margin-top: .2rem 10 | display: flex 11 | 12 | .panel 13 | background: grey 14 | margin-right: 1rem 15 | border-radius: 8px 16 | padding: .3rem 17 | color: white 18 | cursor: pointer 19 | 20 | .content 21 | width: 100% 22 | display: none 23 | margin-top: .5rem 24 | overflow: hidden 25 | transform: translateX(-100vw) 26 | 27 | .show_content 28 | display: block 29 | transform: translateX(0) 30 | transition: -webkit-transform 1s linear 31 | -webkit-transform: translateX(0) 32 | -webkit-transition: transform 1s linear 33 | -moz-transition: transform 1s linear 34 | -o-transition: transform 1s linear 35 | 36 | .filter_box 37 | width: 100% 38 | height: 5vh 39 | display: flex 40 | align-items: center 41 | background: rgba(125,125,125,.2) 42 | position: relative 43 | margin-bottom: 5vh 44 | 45 | .order_box 46 | width: 100% 47 | height: 5vh 48 | display: flex 49 | align-items: center 50 | margin-bottom: .2rem 51 | background: rgba(125,125,125,.2) 52 | 53 | .head_title 54 | max-width: 10rem 55 | margin: 0 1rem 56 | display: flex 57 | img 58 | width: 20px 59 | height: 20px 60 | background: blue 61 | margin-right: .5rem 62 | .head_title_text 63 | font-size: 1rem 64 | 65 | .box 66 | min-width: calc( 100% - 10rem) 67 | height: 100% 68 | display: flex 69 | justify-content: space-around 70 | align-items: center 71 | 72 | .selection 73 | display: flex 74 | flex-direction: column 75 | height: 100% 76 | .sub_selection 77 | height: 0 78 | width: 0 79 | display: none 80 | // width: 100vw 81 | // height: 5vh 82 | position: absolute 83 | top: 100% 84 | left: 0 85 | padding-top: .2rem 86 | overflow: hidden 87 | flex-wrap: wrap 88 | transition: height .2s linear 89 | &:hover, &:focus, &:active 90 | width: 100vw 91 | min-height: 5vh 92 | height: max-content 93 | display: block 94 | &:hover, &:focus, &:active 95 | .sub_selection 96 | width: 100vw 97 | min-height: 5vh 98 | height: max-content 99 | display: block 100 | 101 | @media screen and (max-width: $mobileL) 102 | .head_title 103 | .head_title_text 104 | font-size: .8rem 105 | 106 | .filter_box 107 | margin-bottom: 6vh 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/components/header/stylesheets/header.module.sass: -------------------------------------------------------------------------------- 1 | @import '../../../modules/mediaQuery/app' 2 | 3 | .outbox 4 | display: flex 5 | flex-direction: column 6 | min-width: 320px 7 | width: auto 8 | position: fixed 9 | top: 0 10 | right: 0 11 | left: 0 12 | z-index: 3 13 | background: rgb(125,125,125) 14 | 15 | .content 16 | display: flex 17 | height: 8vh 18 | 19 | .left 20 | display: flex 21 | 22 | .logo 23 | width: 120px 24 | height: 100% 25 | display: flex 26 | justify-content: center 27 | align-items: center 28 | background: pink 29 | margin: 0 1rem 30 | font-size: 1.5rem 31 | &:hover, &:focus 32 | cursor: pointer 33 | 34 | .mid 35 | display: flex 36 | margin: auto 37 | font-size: 1.1rem 38 | color: white !important 39 | 40 | .right 41 | display: flex 42 | align-items: center 43 | margin: 0 1rem 44 | 45 | 46 | @media screen and (max-width: $tablet) 47 | .content 48 | display: flex 49 | justify-content: space-between 50 | align-items: center 51 | 52 | .logo 53 | margin-left: auto 54 | margin-right: 2rem 55 | font-size: 1.3rem 56 | 57 | .toggle_outbox 58 | display: flex 59 | width: 3rem 60 | flex-wrap: nowrap 61 | position: absolute 62 | top: 0 63 | left: 0 64 | right: 0 65 | bottom: 0 66 | 67 | .toggle_icon 68 | height: max-content 69 | margin: .6rem 70 | &:hover, &:blur 71 | .bar1 72 | transform: rotate(-45deg) translate(-9px, 6px) 73 | .bar2 74 | opacity: 0 75 | .bar3 76 | transform: rotate(45deg) translate(-8px, -8px) 77 | 78 | .bar1, .bar2, .bar3 79 | width: 2rem 80 | height: 0.3rem 81 | background-color: #333 82 | margin: 6px 0 83 | transition: 0.4s 84 | 85 | .toggle_content 86 | width: 80vw 87 | min-width: 120px 88 | height: 100vh 89 | background: rgb(125,125,125) 90 | color: white 91 | z-index: 1000 92 | display: flex 93 | flex-direction: column 94 | 95 | .side_title 96 | width: 100% 97 | background: rgb(65,65,65) 98 | font-size: 1rem 99 | padding: .2rem .2rem .2rem .5rem 100 | display: flex 101 | align-items: center 102 | flex-wrap: nowrap 103 | 104 | .side_title_close 105 | font-size: 1.5rem 106 | margin-left: auto 107 | margin-right: 1rem 108 | &:hover 109 | cursor: pointer 110 | 111 | .side_content 112 | padding: 0.8rem 113 | font-size: .8rem 114 | white-space: nowrap 115 | display: flex 116 | align-items: center 117 | border-bottom: 1px solid rgba(0,0,0,.5) 118 | &:hover 119 | cursor: pointer 120 | color: #0056b3 121 | 122 | #toggle 123 | background-color: #FFE600 124 | position: fixed 125 | top: 0 126 | left: 0 127 | overflow: scroll 128 | 129 | .hide 130 | transform: translate3d(-80vw, 0, 0) 131 | transition: transform .5s linear 132 | 133 | .show 134 | transform: translate3d(0vw, 0, 0) 135 | transition: transform .5s linear 136 | + .toggle_icon 137 | display: none 138 | 139 | -------------------------------------------------------------------------------- /src/redux/reducer/productReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_ALL_PRODUCTS_BEGIN, 3 | GET_ALL_PRODUCTS_SUCCESS, 4 | GET_ALL_PRODUCTS_FAIL, 5 | GET_PRODUCT_BEGIN, 6 | GET_PRODUCT_SUCCESS, 7 | GET_PRODUCT_FAIL, 8 | GET_PRODUCTS_BY_CATEGORY_BEGIN, 9 | GET_PRODUCTS_BY_CATEGORY_SUCCESS, 10 | GET_PRODUCTS_BY_CATEGORY_FAIL, 11 | SEARCH_BEGIN, 12 | SEARCH_SUCCESS, 13 | SEARCH_FAIL, 14 | APPLY_FILTERS_BEGIN, 15 | APPLY_FILTERS_SUCCESS, 16 | APPLY_FILTERS_FAIL, 17 | } from '../action/productAction' 18 | 19 | const initialState = { 20 | products: null, 21 | product: null, 22 | loading: false, 23 | error: null, 24 | } 25 | 26 | export default (state = initialState, action) => { 27 | switch (action.type) { 28 | case GET_ALL_PRODUCTS_BEGIN: 29 | return { 30 | ...state, 31 | loading: true, 32 | error: null 33 | } 34 | case GET_ALL_PRODUCTS_SUCCESS: 35 | return { 36 | ...state, 37 | loading: false, 38 | products: action.payload.data.products 39 | } 40 | case GET_ALL_PRODUCTS_FAIL: 41 | return { 42 | ...state, 43 | loading: false, 44 | error: action.payload.error.response.data 45 | } 46 | case GET_PRODUCT_BEGIN: 47 | return { 48 | ...state, 49 | loading: true, 50 | error: null 51 | } 52 | case GET_PRODUCT_SUCCESS: 53 | return { 54 | ...state, 55 | loading: false, 56 | product: action.payload.data.product 57 | } 58 | case GET_PRODUCT_FAIL: 59 | return { 60 | ...state, 61 | loading: false, 62 | error: action.payload.error.response.data 63 | } 64 | case GET_PRODUCTS_BY_CATEGORY_BEGIN: 65 | return { 66 | ...state, 67 | loading: true, 68 | error: null 69 | } 70 | case GET_PRODUCTS_BY_CATEGORY_SUCCESS: 71 | return { 72 | ...state, 73 | loading: false, 74 | products: action.payload.data.products 75 | } 76 | case GET_PRODUCTS_BY_CATEGORY_FAIL: 77 | return { 78 | ...state, 79 | loading: false, 80 | error: action.payload.error.response.data 81 | } 82 | case SEARCH_BEGIN: 83 | return { 84 | ...state, 85 | loading: true, 86 | error: null 87 | } 88 | case SEARCH_SUCCESS: 89 | return { 90 | ...state, 91 | loading: false, 92 | products: action.payload.data.products 93 | } 94 | case SEARCH_FAIL: 95 | return { 96 | ...state, 97 | loading: false, 98 | error: action.payload.error.response.data 99 | } 100 | case APPLY_FILTERS_BEGIN: 101 | return { 102 | ...state, 103 | loading: true, 104 | error: null 105 | } 106 | case APPLY_FILTERS_SUCCESS: 107 | return { 108 | ...state, 109 | loading: false, 110 | products: action.payload.data.products 111 | } 112 | case APPLY_FILTERS_FAIL: 113 | return { 114 | ...state, 115 | loading: false, 116 | error: action.payload.error.response.data 117 | } 118 | default: 119 | return state 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/redux/action/productAction.js: -------------------------------------------------------------------------------- 1 | import serverCall from '../../modules/serverCall' 2 | 3 | export const getAllProducts=()=>dispatch=>{ 4 | dispatch({ 5 | type:GET_ALL_PRODUCTS_BEGIN, 6 | }) 7 | return serverCall({ 8 | method:'GET', 9 | url:`/products` 10 | }) 11 | .then(res=>{ 12 | dispatch({ 13 | type: GET_ALL_PRODUCTS_SUCCESS, 14 | payload: res 15 | }) 16 | return res 17 | }) 18 | .catch(error=>{ 19 | dispatch({ 20 | type: GET_ALL_PRODUCTS_FAIL, 21 | payload: {error} 22 | }) 23 | return error 24 | }) 25 | } 26 | 27 | export const getProduct=(id)=>dispatch=>{ 28 | dispatch({ 29 | type:GET_PRODUCT_BEGIN, 30 | }) 31 | return serverCall({ 32 | method:'GET', 33 | url:`/products/${id}` 34 | }) 35 | .then(res=>{ 36 | dispatch({ 37 | type: GET_PRODUCT_SUCCESS, 38 | payload: res 39 | }) 40 | return res 41 | }) 42 | .catch(error=>{ 43 | dispatch({ 44 | type: GET_PRODUCT_FAIL, 45 | payload: {error} 46 | }) 47 | return error 48 | }) 49 | } 50 | 51 | export const getProductsByCategory=(c)=>dispatch=>{ 52 | dispatch({ 53 | type:GET_PRODUCTS_BY_CATEGORY_BEGIN, 54 | }) 55 | return serverCall({ 56 | method:'GET', 57 | url:`/products?category=${c}` 58 | }) 59 | .then(res=>{ 60 | dispatch({ 61 | type: GET_PRODUCTS_BY_CATEGORY_SUCCESS, 62 | payload: res 63 | }) 64 | return res 65 | }) 66 | .catch(error=>{ 67 | dispatch({ 68 | type: GET_PRODUCTS_BY_CATEGORY_FAIL, 69 | payload: {error} 70 | }) 71 | return error 72 | }) 73 | } 74 | 75 | export const search=(text)=>dispatch=>{ 76 | dispatch({ 77 | type:SEARCH_BEGIN, 78 | }) 79 | return serverCall({ 80 | method:'GET', 81 | url:`/search?query=${text}` 82 | }) 83 | .then(res=>{ 84 | dispatch({ 85 | type: SEARCH_SUCCESS, 86 | payload: res 87 | }) 88 | return res 89 | }) 90 | .catch(error=>{ 91 | dispatch({ 92 | type: SEARCH_FAIL, 93 | payload: {error} 94 | }) 95 | return error 96 | }) 97 | } 98 | 99 | export const applyFilters=(filter_string)=>dispatch=>{ 100 | dispatch({ 101 | type:APPLY_FILTERS_BEGIN, 102 | }) 103 | return serverCall({ 104 | method:'GET', 105 | url:`/products?${filter_string}` 106 | }) 107 | .then(res=>{ 108 | dispatch({ 109 | type: APPLY_FILTERS_SUCCESS, 110 | payload: res 111 | }) 112 | return res 113 | }) 114 | .catch(error=>{ 115 | dispatch({ 116 | type: APPLY_FILTERS_FAIL, 117 | payload: {error} 118 | }) 119 | return error 120 | }) 121 | } 122 | 123 | export const APPLY_FILTERS_BEGIN='APPLY_FILTERS_BEGIN' 124 | export const APPLY_FILTERS_SUCCESS='APPLY_FILTERS_SUCCESS' 125 | export const APPLY_FILTERS_FAIL='APPLY_FILTERS_FAIL' 126 | 127 | 128 | export const SEARCH_BEGIN='SEARCH_BEGIN' 129 | export const SEARCH_SUCCESS='SEARCH_SUCCESS' 130 | export const SEARCH_FAIL='SEARCH_FAIL' 131 | 132 | 133 | export const GET_ALL_PRODUCTS_BEGIN='GET_ALL_PRODUCTS_BEGIN' 134 | export const GET_ALL_PRODUCTS_SUCCESS='GET_ALL_PRODUCTS_SUCCESS' 135 | export const GET_ALL_PRODUCTS_FAIL='GET_ALL_PRODUCTS_FAIL' 136 | 137 | export const GET_PRODUCT_BEGIN='GET_PRODUCT_BEGIN' 138 | export const GET_PRODUCT_SUCCESS='GET_PRODUCT_SUCCESS' 139 | export const GET_PRODUCT_FAIL='GET_PRODUCT_FAIL' 140 | 141 | export const GET_PRODUCTS_BY_CATEGORY_BEGIN='GET_PRODUCTS_BY_CATEGORY_BEGIN' 142 | export const GET_PRODUCTS_BY_CATEGORY_SUCCESS='GET_PRODUCTS_BY_CATEGORY_SUCCESS' 143 | export const GET_PRODUCTS_BY_CATEGORY_FAIL='GET_PRODUCTS_BY_CATEGORY_FAIL' -------------------------------------------------------------------------------- /src/pages/productOverview/ProductOverview.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styles from './stylesheets/productOverview.module.sass' 3 | import { Button } from 'react-bootstrap' 4 | import Header from '../../components/header/headerContainer' 5 | import Variants from './components/Variants' 6 | import mergeProductAndVariants from './utils/mergeProductAndVariants' 7 | import jumpTo from '../../modules/Navigation' 8 | 9 | export default class ProductOverview extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | color: '', 14 | size: '', 15 | pic: '', 16 | selectedSize: '', 17 | id: '' 18 | } 19 | } 20 | componentDidMount() { 21 | this.props.getProduct(this.props.location.pathname.split("/").slice(-1)[0]) 22 | this.props.getVariantsByProductId(this.props.location.pathname.split("/").slice(-1)[0]) 23 | } 24 | 25 | 26 | handleClick = (variant) => { 27 | this.setState({ 28 | color: variant.color, 29 | size: variant.size, 30 | pic: variant.imagePath, 31 | selectedSize: '', 32 | id: variant._id 33 | }) 34 | } 35 | 36 | clickSize = (s) => { 37 | this.setState({ 38 | selectedSize: s 39 | }) 40 | } 41 | 42 | addToBag = () => { 43 | this.props.postCart( 44 | this.state.id || this.props.location.pathname.split("/").slice(-1)[0] 45 | ).then(res => { 46 | jumpTo('/bag') 47 | }) 48 | } 49 | 50 | render() { 51 | return ( 52 |
53 |
54 | {this.props.product && 55 |
56 |
57 | {/* left image */} 58 |
59 | 60 |
61 | {/* right content box */} 62 |
63 |
64 | {/* top descriptions */} 65 |
66 | {this.props.product.title} 67 |
68 |
69 | {this.props.product.description} 70 |
71 |
72 | ${this.props.product.price} CAD 73 |
74 | {/* dotted border */} 75 |
76 | {/* bottom descriptions */} 77 |
78 | 86 |
87 |
88 | 89 | 90 |
91 |
92 |
93 |
94 |
95 | } 96 |
97 | ) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/pages/loginsignin/LoginSignin.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styles from './stylesheets/loginsignin.module.sass' 3 | import Base from './components/Base' 4 | import capitalizeString from './utils/capitalizeString' 5 | import jumpTo from '../../modules/Navigation' 6 | import LoadingAnimation from '../../components/loadingAnimation' 7 | 8 | 9 | export default class LoginSignin extends Component { 10 | constructor(props) { 11 | super(props) 12 | this.state = {} 13 | this.inputText = {} 14 | for (const input of props.INPUT_CONFIG) { 15 | this.state[input.name] = { errorMsg: '' } 16 | this.inputText[input.name] = '' 17 | } 18 | } 19 | //validate input when blur 20 | handleBlur = (e, validResult) => { 21 | const name = e.target.name 22 | this.inputText[name] = e.target.value 23 | if (!validResult.isValid) { 24 | this.setState({ 25 | [name]: { errorMsg: validResult.errorMsg } 26 | }) 27 | } else { 28 | this.setState({ 29 | [name]: { errorMsg: '' } 30 | }) 31 | } 32 | } 33 | // when focus, clear error message 34 | handleFocus = (e) => { 35 | const name = e.target.name 36 | this.setState({ 37 | [name]: { errorMsg: '' } 38 | }) 39 | } 40 | //submit actions 41 | handleClick = () => { 42 | //validate all input 43 | let canSubmit = true 44 | for (const input of this.props.INPUT_CONFIG) { 45 | if (!!!input.validations) continue 46 | for (const v of input.validations) { 47 | let checkResult = v.check(this.inputText[input.name]) 48 | canSubmit = canSubmit && checkResult 49 | if (!checkResult) { 50 | this.setState({ 51 | [input.name]: { errorMsg: v.errMsg } 52 | }) 53 | break 54 | } 55 | } 56 | } 57 | if (!canSubmit) { 58 | console.log('valid fail'); 59 | return 60 | } 61 | if (this.props.title === 'Login') { 62 | const { email, password } = this.inputText 63 | this.props.submitAction(email, password) 64 | .then(res => { 65 | jumpTo('/dashboard') 66 | // console.log(res) 67 | // console.log('loginsignin res'); 68 | return res 69 | }) 70 | .catch(error => { 71 | // console.log('loginsignin error') 72 | // console.log(error.response) 73 | alert(error.response.data.error.message) 74 | return error 75 | }) 76 | } 77 | if (this.props.title === 'Signin') { 78 | const { fullname, email, password, verifyPassword } = this.inputText 79 | this.props.submitAction(fullname, email, password, verifyPassword) 80 | .then(res => { 81 | jumpTo('/login') 82 | }) 83 | .catch(error => { 84 | alert(error.response.data.error.message) 85 | return error 86 | }) 87 | } 88 | } 89 | render() { 90 | return ( 91 |
92 |
93 | {this.props.loading && 94 | 95 | } 96 | 106 | {this.props.footer_text} 107 | {capitalizeString(this.props.footer_redirect)} 108 | 109 |
110 | } 111 | /> 112 |
113 |
114 | ) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/Filter_md.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styles from '../stylesheets/filter_md.module.sass' 3 | import Selection from './Selection' 4 | import SubSelection from './SubSelection' 5 | 6 | export default class Filter_md extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | isShow: false 11 | } 12 | } 13 | clickShow = () => { 14 | this.setState(prevState => ({ 15 | isShow: !prevState.isShow 16 | })) 17 | } 18 | clickClear = () => { 19 | this.props.clear() 20 | } 21 | render() { 22 | const { onChange, clear, selected_name, configs } = this.props 23 | return ( 24 |
25 |
26 |
30 | {this.state.isShow 31 | ?
Close Filter
32 | :
Show Filter
33 | } 34 |
35 |
39 | Clear Filter 40 |
41 |
42 |
43 | {/* order */} 44 |
45 |
46 | 47 |
48 | ORDER 49 |
50 |
51 |
52 | {configs['order'].map(n => { 53 | n = n.toUpperCase() 54 | return ( 55 | 62 | ) 63 | })} 64 |
65 |
66 | {/* filter */} 67 |
68 |
69 | 70 |
71 | FILTER 72 |
73 |
74 |
75 | {/* department */} 76 |
77 | 0} 80 | onChange={() => { }} 81 | /> 82 |
83 | 90 |
91 |
92 | {/* price */} 93 |
94 | 0} 97 | onChange={() => { }} 98 | /> 99 |
100 | 107 |
108 |
109 |
110 |
111 |
112 |
113 | ) 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /src/assets/images/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 26 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 61 | 69 | 85 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/components/header/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styles from './stylesheets/header.module.sass' 3 | import UserHeader from './components/UserHeader' 4 | import Menu from './components/Menu' 5 | import Search from './components/Search' 6 | import jumpTo,{go} from '../../modules/Navigation' 7 | import Auth from '../../modules/Auth' 8 | import device, { size } from '../../modules/mediaQuery' 9 | import MediaQuery from 'react-responsive' 10 | 11 | export default class Header extends Component { 12 | constructor(props) { 13 | super(props) 14 | this.state = { 15 | input: '', 16 | isToggle: false 17 | } 18 | } 19 | handleChange = (v) => { 20 | this.setState({ 21 | input: v 22 | }) 23 | } 24 | handleSuggest = (v) => { 25 | this.setState({ 26 | input: v 27 | }) 28 | } 29 | handleToggle = () => { 30 | this.setState(prevState => { 31 | return { 32 | isToggle: !prevState.isToggle 33 | } 34 | }) 35 | } 36 | closeToggle = () => { 37 | this.setState({ 38 | isToggle: false 39 | }) 40 | } 41 | render() { 42 | const { user_token, 43 | departments, 44 | search, 45 | getProductsByCategory, 46 | getAllProducts } = this.props 47 | let visibility = "hide" 48 | if (this.state.isToggle) { 49 | visibility = "show" 50 | } 51 | return ( 52 |
53 | {/* larger than 768px */} 54 | 55 | {/* top user header */} 56 |
57 | 60 |
61 | {/* menu header */} 62 |
63 |
64 | {/* logo */} 65 |
{ 67 | getAllProducts() 68 | jumpTo('/dashboard') 69 | }} 70 | > 71 | Zack Market 72 |
73 |
74 |
75 | 80 |
81 |
82 | 88 |
89 |
90 |
91 | {/* smaller than 768px */} 92 | 93 |
94 |
95 | {/* toggle content */} 96 |
97 |
98 |
99 | MENU 100 |
104 | x 105 |
106 |
107 | 113 |
114 | CATEGORY 115 |
116 | 121 |
122 | CART 123 |
124 |
jumpTo('/bag')} 127 | > 128 | Shopping Bag 129 |
130 |
131 | USER 132 |
133 |
jumpTo('/login')} 136 | > 137 | Login 138 |
139 |
{ Auth.logout(); go('/dashboard') }} 142 | > 143 | Logout 144 |
145 |
146 |
147 | {/* toggle icon */} 148 |
149 |
150 |
151 |
152 |
153 |
154 | {/* logo */} 155 |
{ 157 | getAllProducts() 158 | jumpTo('/dashboard') 159 | }} 160 | > 161 | Zack Market 162 |
163 |
164 |
165 |
166 | ) 167 | } 168 | } 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /src/pages/dashboard/components/Filter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Checkbox from './Checkbox' 3 | import styles from '../stylesheets/filter.module.sass' 4 | import generateFilterString from '../utils/generateFilterString' 5 | import device, { size } from '../../../modules/mediaQuery' 6 | import MediaQuery from 'react-responsive' 7 | import Filter_md from './Filter_md' 8 | 9 | export default class Filter extends Component { 10 | constructor(props) { 11 | super(props) 12 | this.FILTER_CONFIG = { 13 | order: ['Ascending', 'Descending'], 14 | department: ['Men', 'Women'], 15 | price: ['Less Than $29', '$29 - $39', '$39 - $49', '$49 - $89', 'Greater Than $89'] 16 | } 17 | this.initialState = {} 18 | this.state = this.initialState 19 | } 20 | handleChange = (e, category, name) => { 21 | let tagName = '' 22 | let isChecked = false 23 | //handle div click 24 | if (name) { 25 | tagName = name.toUpperCase() 26 | isChecked = !!!(this.state[category] && this.state[category].includes(name)) 27 | } else { 28 | // handle input checkbox 29 | tagName = e.target.name.toUpperCase() 30 | isChecked = e.target.checked 31 | } 32 | this.setState(prevState => { 33 | //add category value to array 34 | if (isChecked) { 35 | //user can only select one order 36 | if (category === 'order') { 37 | return { 38 | [category]: [tagName] 39 | } 40 | } 41 | return { 42 | [category]: [...prevState[category] || [], tagName] 43 | } 44 | } else { 45 | //remove category value from array 46 | const new_prop_array = prevState[category].filter(n => n !== tagName) 47 | return { 48 | [category]: new_prop_array 49 | } 50 | } 51 | }, () => { 52 | this.props.applyFilters(generateFilterString(this.state)) 53 | }) 54 | } 55 | handleCloseTag = (category, name) => { 56 | this.setState(prevState => { 57 | const new_prop_array = prevState[category].filter(n => n !== name) 58 | return { 59 | [category]: new_prop_array 60 | } 61 | }, () => this.props.applyFilters(generateFilterString(this.state))) 62 | } 63 | clearAllFilter = () => { 64 | this.setState({ order: [], price: [], department: [] }, () => this.props.applyFilters('')) 65 | } 66 | render() { 67 | return ( 68 |
69 | 70 |
71 |
72 |
73 | FILTERS 74 |
75 |
76 |
77 | {/* order */} 78 |
79 |
80 | ORDER 81 |
82 | {this.FILTER_CONFIG['order'].map(n => 83 | 90 | )} 91 |
92 | {/* department */} 93 |
94 |
95 | DEPARTMENT 96 |
97 | {this.FILTER_CONFIG['department'].map(n => 98 | 105 | )} 106 |
107 | {/* price */} 108 |
109 |
110 | PRICE 111 |
112 | {this.FILTER_CONFIG['price'].map(n => 113 | 120 | )} 121 |
122 |
123 |
124 | 125 |
126 | {/* filter tags */} 127 |
128 | { 129 | Object.keys(this.state).map(c => ( 130 | this.state[c] && this.state[c].map(n => ( 131 |
132 |
133 | {n} 134 |
135 |
this.handleCloseTag(c, n)} 138 | > 139 | x 140 |
141 |
142 | ))))} 143 |
144 |
145 |
146 |
147 | 148 | 154 | 155 |
156 | ) 157 | } 158 | } 159 | 160 | --------------------------------------------------------------------------------