├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── redux │ ├── user │ │ ├── user.types.js │ │ ├── user.actions.js │ │ ├── user.selector.js │ │ └── user.reducer.js │ ├── shop │ │ ├── shop.selectors.js │ │ ├── shop.reducer.js │ │ └── shop.data.js │ ├── cart │ │ ├── card.types.js │ │ ├── cart.actions.js │ │ ├── cart.selectors.js │ │ ├── cart.utils.js │ │ └── cart.reducer.js │ ├── directory │ │ ├── directory.selector.jsx │ │ └── directory.reducer.jsx │ ├── store.js │ └── root-reducer.js ├── components │ ├── collections-overview │ │ ├── collections.overview.styles.scss │ │ └── collections-overview.component.jsx │ ├── directory │ │ ├── directory.styles.scss │ │ └── directory.component.jsx │ ├── sign-up │ │ ├── sign-up.styles.scss │ │ └── sign-up.component.jsx │ ├── sign-in │ │ ├── sign-in.styles.scss │ │ └── sign-in.component.jsx │ ├── preview-collection │ │ ├── collection-preview.styles.scss │ │ └── collection-preview.component.jsx │ ├── cart-icon │ │ ├── cart-icon.styles.scss │ │ └── cart-icon.component.jsx │ ├── cart-item │ │ ├── cart-item.styles.scss │ │ └── cart-item.component.jsx │ ├── custom-button │ │ ├── custom-button.component.jsx │ │ └── custom-button.styles.scss │ ├── header │ │ ├── header.styles.scss │ │ └── header.component.jsx │ ├── cart-dropdown │ │ ├── cart-dropdown.styles.scss │ │ └── cart-dropdown.component.jsx │ ├── form-input │ │ ├── form-input.component.jsx │ │ └── form-input.styles.scss │ ├── menu-item │ │ ├── menu-item.component.jsx │ │ └── menu-item.styles.scss │ ├── checkout-item │ │ ├── checkout-item.styles.scss │ │ └── checkout-item.component.jsx │ └── collection-item │ │ ├── collection-item.styles.scss │ │ └── collection-item.component.jsx ├── pages │ ├── homepage │ │ ├── homepage.styles.scss │ │ └── homepage.component.jsx │ ├── sign-in-sign-up │ │ ├── sign-in-sign-up.styles.scss │ │ └── sign-in-sign-up.component.jsx │ ├── shop │ │ └── shop.component.jsx │ └── checkout │ │ ├── checkout.styles.scss │ │ └── checkout.component.jsx ├── App.css ├── index.css ├── index.js ├── setupTests.js ├── App.test.js ├── firebase │ └── firebase.utils.js ├── assets │ ├── shopping-bag.svg │ └── crown.svg ├── App.js ├── logo.svg └── serviceWorker.js ├── .gitignore ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: / -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bruceallday/ReactJS-ecommerce-template/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bruceallday/ReactJS-ecommerce-template/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bruceallday/ReactJS-ecommerce-template/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/redux/user/user.types.js: -------------------------------------------------------------------------------- 1 | export const UserActionTypes = { 2 | SET_CURRENT_USER: "SET_CURRENT_USER" 3 | }; -------------------------------------------------------------------------------- /src/components/collections-overview/collections.overview.styles.scss: -------------------------------------------------------------------------------- 1 | .collections-overview{ 2 | display: flex; 3 | flex-direction: column; 4 | } -------------------------------------------------------------------------------- /src/components/directory/directory.styles.scss: -------------------------------------------------------------------------------- 1 | .directory-menu { 2 | width: 100%; 3 | display: flex; 4 | flex-wrap: wrap; 5 | justify-content: space-between; 6 | } -------------------------------------------------------------------------------- /src/pages/homepage/homepage.styles.scss: -------------------------------------------------------------------------------- 1 | .homepage { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | padding: 20px 80px; 6 | } 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/pages/sign-in-sign-up/sign-in-sign-up.styles.scss: -------------------------------------------------------------------------------- 1 | .sign-in-and-sign-up{ 2 | width: 850px; 3 | display: flex; 4 | justify-content: space-between; 5 | margin: 30px auto; 6 | } -------------------------------------------------------------------------------- /src/components/sign-up/sign-up.styles.scss: -------------------------------------------------------------------------------- 1 | .sign-up{ 2 | display: flex; 3 | flex-direction: column; 4 | width: 380px; 5 | 6 | .title{ 7 | margin: 10px 0px; 8 | } 9 | } -------------------------------------------------------------------------------- /src/redux/user/user.actions.js: -------------------------------------------------------------------------------- 1 | import { UserActionTypes } from './user.types.js' 2 | 3 | export const setCurrentUser = user => ({ 4 | type: UserActionTypes.SET_CURRENT_USER, 5 | payload: user 6 | }) -------------------------------------------------------------------------------- /src/redux/shop/shop.selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | const selectShop = state => state.shop 4 | 5 | export const selectCollections = createSelector( 6 | [selectShop], 7 | shop => shop.collections 8 | ); 9 | -------------------------------------------------------------------------------- /src/redux/user/user.selector.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | const selectUser = state => state.user; 4 | 5 | export const selectCurrentUser = createSelector( 6 | [selectUser], 7 | user => user.currentUser 8 | ) 9 | 10 | -------------------------------------------------------------------------------- /src/redux/cart/card.types.js: -------------------------------------------------------------------------------- 1 | const CartActionTypes = { 2 | TOGGLE_CART_HIDDEN: "TOGGLE_CART_HIDDEN", 3 | ADD_ITEM: "ADD_ITEM", 4 | REMOVE_ITEM: "REMOVE_ITEM", 5 | CLEAR_ITEM_FROM_CART: "CLEAR_ITEM_FROM_CART" 6 | }; 7 | 8 | export default CartActionTypes; 9 | -------------------------------------------------------------------------------- /src/redux/directory/directory.selector.jsx: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | const selectDirectory = state => state.directory 4 | 5 | export const selectDirectorySections = createSelector( 6 | [selectDirectory], 7 | directory => directory.sections 8 | ) -------------------------------------------------------------------------------- /src/components/sign-in/sign-in.styles.scss: -------------------------------------------------------------------------------- 1 | .sign-in{ 2 | width: 380px; 3 | display: flex; 4 | flex-direction: column; 5 | 6 | .title{ 7 | margin: 10px 0px; 8 | } 9 | 10 | .buttons{ 11 | display: flex; 12 | justify-content: space-between; 13 | } 14 | } -------------------------------------------------------------------------------- /src/pages/homepage/homepage.component.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Directory from '../../components/directory/directory.component' 4 | 5 | import "./homepage.styles.scss"; 6 | 7 | const HomePage = () => ( 8 |
9 | 10 |
11 | ); 12 | 13 | export default HomePage; 14 | -------------------------------------------------------------------------------- /src/pages/shop/shop.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CollectionsOverview from '../../components/collections-overview/collections-overview.component' 3 | 4 | const ShopPage = ({ collections }) => ( 5 |
6 | 7 |
8 | ) 9 | 10 | export default ShopPage; -------------------------------------------------------------------------------- /src/redux/shop/shop.reducer.js: -------------------------------------------------------------------------------- 1 | import SHOP_DATA from './shop.data'; 2 | 3 | const INITIAL_STATE = { 4 | collections: SHOP_DATA 5 | }; 6 | 7 | const shopReducer = (state = INITIAL_STATE, action) => { 8 | switch (action.type) { 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default shopReducer; -------------------------------------------------------------------------------- /src/components/preview-collection/collection-preview.styles.scss: -------------------------------------------------------------------------------- 1 | .collection-preview { 2 | display: flex; 3 | flex-direction: column; 4 | margin-bottom: 30px; 5 | 6 | .title { 7 | font-size: 28px; 8 | margin-bottom: 25px; 9 | } 10 | 11 | .preview { 12 | display: flex; 13 | justify-content: space-between; 14 | } 15 | } -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main-bg-color: #FFFFFF; 3 | } 4 | .dark :root { 5 | --main-bg-color: #000000; 6 | } 7 | 8 | body { 9 | font-family: 'Open Sans Condensed', sans-serif; 10 | padding: 20px 60px; 11 | background-color: var(--main-bg-color); 12 | } 13 | 14 | a { 15 | text-decoration: none; 16 | color: black; 17 | } 18 | 19 | * { 20 | box-sizing: border-box; 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/cart-icon/cart-icon.styles.scss: -------------------------------------------------------------------------------- 1 | .cart-icon { 2 | width: 45px; 3 | height: 45px; 4 | position: relative; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | cursor: pointer; 9 | 10 | .shopping-icon { 11 | width: 24px; 12 | height: 24px; 13 | } 14 | 15 | .item-count { 16 | position: absolute; 17 | font-size: 10px; 18 | font-weight: bold; 19 | bottom: 12px; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/cart-item/cart-item.styles.scss: -------------------------------------------------------------------------------- 1 | .cart-item { 2 | width: 100%; 3 | display: flex; 4 | height: 80px; 5 | margin-bottom: 15px; 6 | 7 | img { 8 | width: 30%; 9 | } 10 | 11 | .item-details { 12 | width: 70%; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: flex-start; 16 | justify-content: center; 17 | padding: 10px 20px; 18 | 19 | .name { 20 | font-size: 16px; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/pages/sign-in-sign-up/sign-in-sign-up.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import SignIn from '../../components/sign-in/sign-in.component'; 4 | import SignUp from '../../components/sign-up/sign-up.component'; 5 | 6 | import './sign-in-sign-up.styles.scss'; 7 | 8 | const SignInAndSignUp = () => ( 9 |
10 | 11 | 12 |
13 | ) 14 | 15 | export default SignInAndSignUp; 16 | -------------------------------------------------------------------------------- /src/components/custom-button/custom-button.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './custom-button.styles.scss'; 3 | 4 | const CustomButton = ({ 5 | children, 6 | isGoogleSignIn, 7 | inverted, 8 | ...otherProps 9 | }) => ( 10 | 18 | ); 19 | 20 | export default CustomButton; -------------------------------------------------------------------------------- /src/components/cart-item/cart-item.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './cart-item.styles.scss' 4 | 5 | const CartItem = ({ item: { imageUrl, price, name, quantity } }) => ( 6 |
7 | cart item 8 |
9 | {name} 10 | 11 | {quantity} x ${price} 12 | 13 |
14 |
15 | ); 16 | 17 | export default CartItem -------------------------------------------------------------------------------- /src/redux/user/user.reducer.js: -------------------------------------------------------------------------------- 1 | import { UserActionTypes } from './user.types.js' 2 | 3 | const INITIAL_STATE = { 4 | currentUser: null 5 | } 6 | 7 | const userReducer = (state = INITIAL_STATE, action ) => { 8 | switch(action.type){ 9 | case UserActionTypes.SET_CURRENT_USER: 10 | return{ 11 | ...state, 12 | currentUser: action.payload 13 | } 14 | default: 15 | return state; 16 | } 17 | } 18 | 19 | export default userReducer; -------------------------------------------------------------------------------- /src/redux/cart/cart.actions.js: -------------------------------------------------------------------------------- 1 | import CartActionTypes from './card.types' 2 | 3 | export const toggleCartHidden = () => ({ 4 | type: CartActionTypes.TOGGLE_CART_HIDDEN 5 | }) 6 | 7 | export const addItem = item =>({ 8 | type: CartActionTypes.ADD_ITEM, 9 | payload: item 10 | }) 11 | 12 | export const removeItem = item =>({ 13 | type: CartActionTypes.REMOVE_ITEM, 14 | payload: item 15 | }) 16 | 17 | export const clearItemFromCart = item => ({ 18 | type: CartActionTypes.CLEAR_ITEM_FROM_CART, 19 | payload: item 20 | }) 21 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { persistStore } from 'redux-persist'; 3 | import logger from 'redux-logger'; 4 | 5 | import rootReducer from './root-reducer'; 6 | 7 | const middlewares = []; 8 | if (process.env.NODE_ENV !== 'production' && !process.env.STORE_LOG_DISABLE) { 9 | middlewares.push(logger) 10 | } 11 | 12 | export const store = createStore(rootReducer, applyMiddleware(...middlewares)); 13 | export const persistor = persistStore(store); 14 | 15 | export default { store, persistor }; -------------------------------------------------------------------------------- /src/components/header/header.styles.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | height: 70px; 3 | width: 100%; 4 | display: flex; 5 | justify-content: space-between; 6 | margin-bottom: 25px; 7 | 8 | .logo-container { 9 | height: 100%; 10 | width: 70px; 11 | padding: 25px; 12 | } 13 | 14 | .options { 15 | width: 50%; 16 | height: 100%; 17 | display: flex; 18 | align-items: center; 19 | justify-content: flex-end; 20 | cursor: pointer; 21 | 22 | .option { 23 | padding: 10px 15px; 24 | } 25 | .king{ 26 | text-align : center; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/cart-dropdown/cart-dropdown.styles.scss: -------------------------------------------------------------------------------- 1 | .cart-dropdown { 2 | position: absolute; 3 | width: 240px; 4 | height: 340px; 5 | display: flex; 6 | flex-direction: column; 7 | padding: 20px; 8 | border: 1px solid black; 9 | background-color: white; 10 | top: 90px; 11 | right: 40px; 12 | z-index: 5; 13 | 14 | .empty-message{ 15 | font-size: 18px; 16 | margin: 50px auto; 17 | } 18 | 19 | .cart-items { 20 | height: 240px; 21 | display: flex; 22 | flex-direction: column; 23 | overflow: scroll; 24 | } 25 | 26 | button { 27 | margin-top: auto; 28 | } 29 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import { PersistGate } from 'redux-persist/integration/react' 6 | import {store, persistor} from './redux/store.js'; 7 | 8 | import './index.css'; 9 | import App from './App'; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | , 19 | 20 | document.getElementById("root") 21 | ); 22 | 23 | -------------------------------------------------------------------------------- /src/components/form-input/form-input.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './form-input.styles.scss'; 4 | 5 | const FormInput = ({ handleChange, label, ...otherProps }) => ( 6 |
7 | 8 | {label ? ( 9 | 16 | ) : null} 17 |
18 | ); 19 | 20 | export default FormInput; -------------------------------------------------------------------------------- /src/pages/checkout/checkout.styles.scss: -------------------------------------------------------------------------------- 1 | .checkout-page { 2 | width: 55%; 3 | min-height: 90vh; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | margin: 50px auto 0; 8 | 9 | .checkout-header { 10 | width: 100%; 11 | padding: 10px 0; 12 | display: flex; 13 | justify-content: space-between; 14 | border-bottom: 1px solid darkgrey; 15 | 16 | .header-block { 17 | text-transform: capitalize; 18 | width: 23%; 19 | 20 | &:last-child { 21 | width: 8%; 22 | } 23 | } 24 | } 25 | 26 | .total { 27 | margin-top: 30px; 28 | margin-left: auto; 29 | font-size: 36px; 30 | } 31 | } -------------------------------------------------------------------------------- /src/components/preview-collection/collection-preview.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import CollectionItem from '../collection-item/collection-item.component'; 4 | 5 | import './collection-preview.styles.scss'; 6 | 7 | const CollectionPreview = ({ title, items }) => ( 8 |
9 |

{title.toUpperCase()}

10 |
11 | {items 12 | .filter((item, idx) => idx < 4 ) 13 | .map((item) => ( 14 | 15 | ))} 16 |
17 |
18 | ) 19 | 20 | export default CollectionPreview -------------------------------------------------------------------------------- /src/components/menu-item/menu-item.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {withRouter} from 'react-router-dom'; 3 | import './menu-item.styles.scss'; 4 | 5 | const MenuItem = ({ title, imageUrl, size, history, linkUrl, match }) => ( 6 |
history.push(`${match.url}${linkUrl}`)}> 8 |
14 |
15 |

{title.toUpperCase()}

16 | SHOP NOW 17 |
18 |
19 | ); 20 | 21 | export default withRouter(MenuItem); -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | import {configure} from 'enzyme'; 7 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; 8 | process.env.STORE_LOG_DISABLE = true; 9 | 10 | configure({adapter: new Adapter()}); 11 | 12 | if (global.document) { 13 | document.createRange = () => ({ 14 | setStart: () => {}, 15 | setEnd: () => {}, 16 | commonAncestorContainer: { 17 | nodeName: 'BODY', 18 | ownerDocument: document, 19 | }, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/redux/root-reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { persistReducer } from 'redux-persist' 3 | import storage from 'redux-persist/lib/storage' 4 | 5 | import userReducer from './user/user.reducer.js' 6 | import cartReducer from './cart/cart.reducer.js' 7 | import directoryReducer from './directory/directory.reducer' 8 | import shopReducer from './shop/shop.reducer' 9 | 10 | 11 | const persistConfig = { 12 | key: 'root', 13 | storage, 14 | whitelist: ['cart'] 15 | } 16 | 17 | const rootReducer = combineReducers({ 18 | user: userReducer, 19 | cart: cartReducer, 20 | directory: directoryReducer, 21 | shop: shopReducer, 22 | }) 23 | 24 | export default persistReducer(persistConfig, rootReducer) -------------------------------------------------------------------------------- /src/components/directory/directory.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from 'reselect' 4 | import { selectDirectorySections } from '../../redux/directory/directory.selector' 5 | import MenuItem from '../menu-item/menu-item.component'; 6 | 7 | import './directory.styles.scss'; 8 | 9 | const Directory = ({ sections }) => ( 10 |
11 | {sections.map(({ id, ...otherSectionProps }) => ( 12 | 13 | ))} 14 |
15 | ) 16 | 17 | const mapStateToProps = createStructuredSelector({ 18 | sections: selectDirectorySections 19 | }) 20 | 21 | export default connect(mapStateToProps)(Directory); 22 | 23 | -------------------------------------------------------------------------------- /src/components/checkout-item/checkout-item.styles.scss: -------------------------------------------------------------------------------- 1 | .checkout-item { 2 | width: 100%; 3 | display: flex; 4 | min-height: 100px; 5 | border-bottom: 1px solid darkgrey; 6 | padding: 15px 0; 7 | font-size: 20px; 8 | align-items: center; 9 | 10 | .image-container { 11 | width: 23%; 12 | padding-right: 15px; 13 | 14 | img { 15 | width: 100%; 16 | height: 100%; 17 | } 18 | } 19 | .name, 20 | .quantity, 21 | .price { 22 | width: 23%; 23 | } 24 | 25 | .quantity { 26 | // padding-left: 20px; 27 | display: flex; 28 | 29 | .arrow{ 30 | cursor: pointer; 31 | } 32 | 33 | .value{ 34 | margin: 0px 10px; 35 | } 36 | } 37 | 38 | .remove-button { 39 | padding-left: 12px; 40 | cursor: pointer; 41 | } 42 | } -------------------------------------------------------------------------------- /src/components/collections-overview/collections-overview.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from 'reselect'; 4 | 5 | import { selectCollections } from '../../redux/shop/shop.selectors' 6 | 7 | import CollectionPreview from '../preview-collection/collection-preview.component'; 8 | 9 | import './collections.overview.styles.scss' 10 | 11 | const CollectionsOverview = ({ collections }) => ( 12 |
13 | {collections.map(({ id, ...otherCollectionProps }) => ( 14 | 15 | ))} 16 |
17 | ) 18 | 19 | const mapStateToProps = createStructuredSelector({ 20 | collections: selectCollections, 21 | }) 22 | 23 | export default connect(mapStateToProps)(CollectionsOverview) -------------------------------------------------------------------------------- /src/redux/cart/cart.selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | const selectCart = state => state.cart; 4 | 5 | export const selectCartItems = createSelector( 6 | [selectCart], 7 | cart => cart.cartItems 8 | ) 9 | 10 | export const selectCartHidden = createSelector( 11 | [selectCart], 12 | cart => cart.hidden 13 | ) 14 | 15 | export const selectCartItemsCount = createSelector( 16 | [selectCartItems], 17 | cartItems => 18 | cartItems.reduce( 19 | (accumulatedQuantity, cartItem) => 20 | accumulatedQuantity + cartItem.quantity, 21 | 0 22 | ) 23 | ) 24 | 25 | export const selectCartTotal = createSelector( 26 | [selectCartItems], 27 | cartItems => 28 | cartItems.reduce( 29 | (accumulatedQuantity, cartItem) => 30 | accumulatedQuantity + cartItem.quantity * cartItem.price, 31 | 0 32 | ) 33 | 34 | ) -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, mount } from 'enzyme/build' 3 | import { render } from '@testing-library/react'; 4 | import App from './App'; 5 | import { store, persistor } from './redux/store.js' 6 | import { BrowserRouter } from 'react-router-dom'; 7 | import { Provider } from 'react-redux'; 8 | import { PersistGate } from 'redux-persist/integration/react' 9 | import CartDropdown from './components/cart-dropdown/cart-dropdown.component'; 10 | 11 | 12 | it('mounts App without crashing', () => { 13 | mount( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | }) 23 | 24 | it('mounts CartDropdown with empty data', () => { 25 | let wrapper = mount() 26 | expect(wrapper.find('.empty-message').last().text()).toEqual('Your cart is empty'); 27 | }) 28 | -------------------------------------------------------------------------------- /src/components/collection-item/collection-item.styles.scss: -------------------------------------------------------------------------------- 1 | .collection-item { 2 | width: 22%; 3 | display: flex; 4 | flex-direction: column; 5 | height: 350px; 6 | align-items: center; 7 | position: relative; 8 | 9 | .image { 10 | width: 100%; 11 | height: 95%; 12 | background-size: cover; 13 | background-position: center; 14 | margin-bottom: 5px; 15 | } 16 | 17 | .custom-button{ 18 | width: 80%; 19 | opacity: 0.7; 20 | position: absolute; 21 | top: 255px; 22 | display: none; 23 | } 24 | 25 | &:hover{ 26 | .image{ 27 | opacity: 0.8; 28 | } 29 | 30 | .custom-button{ 31 | opacity: 0.85; 32 | display: flex; 33 | } 34 | } 35 | 36 | .collection-footer { 37 | width: 100%; 38 | height: 5%; 39 | display: flex; 40 | justify-content: space-between; 41 | font-size: 18px; 42 | 43 | .name { 44 | width: 90%; 45 | margin-bottom: 15px; 46 | } 47 | 48 | .price { 49 | width: 10%; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/custom-button/custom-button.styles.scss: -------------------------------------------------------------------------------- 1 | .custom-button { 2 | min-width: 165px; 3 | width: auto; 4 | height: 50px; 5 | letter-spacing: 0.5px; 6 | line-height: 50px; 7 | padding: 0 35px 0 35px; 8 | font-size: 15px; 9 | background-color: black; 10 | color: white; 11 | text-transform: uppercase; 12 | font-family: 'Open Sans Condensed'; 13 | font-weight: bolder; 14 | border: none; 15 | cursor: pointer; 16 | display: flex; 17 | justify-content: center; 18 | 19 | &:hover { 20 | background-color: white; 21 | color: black; 22 | border: 1px solid black; 23 | } 24 | 25 | &.google-sign-in{ 26 | background-color: #4285f4; 27 | color: white; 28 | 29 | &:hover{ 30 | background-color: #357ae8; 31 | border: none; 32 | } 33 | } 34 | 35 | &.inverted{ 36 | background-color: white; 37 | color: black; 38 | border: 1px solid black; 39 | 40 | &:hover{ 41 | background-color: black; 42 | color: white; 43 | border: none; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/components/cart-icon/cart-icon.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {selectCartItemsCount} from '../../redux/cart/cart.selectors'; 4 | import { createStructuredSelector } from "reselect"; 5 | import { toggleCartHidden } from '../../redux/cart/cart.actions' 6 | 7 | import './cart-icon.styles.scss'; 8 | import { ReactComponent as ShoppingIcon } from '../../assets/shopping-bag.svg'; 9 | 10 | const CartIcon = ({ toggleCartHidden, itemCount }) => ( 11 |
12 | 13 | { itemCount } 14 |
15 | ) 16 | 17 | const mapDispatchToProps = dispatch => ({ 18 | toggleCartHidden: () => dispatch(toggleCartHidden()) 19 | }) 20 | 21 | const mapStateToProps = createStructuredSelector({ 22 | itemCount: selectCartItemsCount 23 | }); 24 | 25 | export default connect ( 26 | mapStateToProps, 27 | mapDispatchToProps) 28 | (CartIcon); 29 | -------------------------------------------------------------------------------- /src/redux/cart/cart.utils.js: -------------------------------------------------------------------------------- 1 | export const addItemsToCart = (cartItems, cartItemToAdd) => { 2 | const existingCartItem = cartItems.find( 3 | cartItem => cartItem.id === cartItemToAdd.id 4 | ) 5 | 6 | if(existingCartItem){ 7 | return cartItems.map(cartItem => 8 | cartItem.id === cartItemToAdd.id 9 | ? { ...cartItem, quantity: cartItem.quantity + 1 } 10 | : cartItem 11 | ) 12 | } 13 | 14 | return [...cartItems, { ...cartItemToAdd, quantity: 1 }] 15 | } 16 | 17 | export const removeItemFromCart = (cartItems, cartItemToRemove) => { 18 | const existingCartItem = cartItems.find( 19 | cartItem => cartItem.id === cartItemToRemove.id 20 | ) 21 | 22 | if(existingCartItem.quantity === 1){ 23 | return cartItems.filter(cartItem => cartItem.id !== cartItemToRemove.id) 24 | } 25 | 26 | return cartItems.map(cartItem => 27 | cartItem.id === cartItemToRemove.id 28 | ? {...cartItem, quantity: cartItem.quantity -1 } 29 | :cartItem 30 | ) 31 | } -------------------------------------------------------------------------------- /src/components/collection-item/collection-item.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import CustomButton from '../custom-button/custom-button.component.jsx'; 4 | import { addItem }from '../../redux/cart/cart.actions.js'; 5 | 6 | import './collection-item.styles.scss'; 7 | 8 | 9 | const CollectionItem = ({item, addItem}) => { 10 | const {name, price, imageUrl} = item; 11 | return ( 12 |
13 |
19 |
20 | {name} 21 | {price} 22 |
23 | 24 | addItem(item)} inverted>Add to cart 25 |
26 | ); 27 | } 28 | 29 | const mapDispatchToProps = dispatch => ({ 30 | addItem: item => dispatch(addItem(item)) 31 | }) 32 | 33 | export default connect(null, mapDispatchToProps) (CollectionItem); -------------------------------------------------------------------------------- /src/components/form-input/form-input.styles.scss: -------------------------------------------------------------------------------- 1 | $sub-color: grey; 2 | $main-color: black; 3 | 4 | @mixin shrinkLabel { 5 | top: -14px; 6 | font-size: 12px; 7 | color: $main-color; 8 | } 9 | 10 | .group { 11 | position: relative; 12 | margin: 45px 0; 13 | 14 | .form-input { 15 | background: none; 16 | background-color: white; 17 | color: $sub-color; 18 | font-size: 18px; 19 | padding: 10px 10px 10px 5px; 20 | display: block; 21 | width: 100%; 22 | border: none; 23 | border-radius: 0; 24 | border-bottom: 1px solid $sub-color; 25 | margin: 25px 0; 26 | 27 | &:focus { 28 | outline: none; 29 | } 30 | 31 | &:focus ~ .form-input-label { 32 | @include shrinkLabel(); 33 | } 34 | } 35 | 36 | input[type='password'] { 37 | letter-spacing: 0.3em; 38 | } 39 | 40 | .form-input-label { 41 | color: $sub-color; 42 | font-size: 16px; 43 | font-weight: normal; 44 | position: absolute; 45 | pointer-events: none; 46 | left: 5px; 47 | top: 10px; 48 | transition: 300ms ease all; 49 | 50 | &.shrink { 51 | @include shrinkLabel(); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommerce-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@wojtekmaj/enzyme-adapter-react-17": "^0.4.1", 10 | "enzyme": "^3.11.0", 11 | "firebase": "^7.6.1", 12 | "node-sass": "^9.0.0", 13 | "react": "^17.0.1", 14 | "react-dom": "^17.0.1", 15 | "react-redux": "^7.2.2", 16 | "react-router-dom": "^5.2.0", 17 | "redux": "^4.0.5", 18 | "redux-logger": "^3.0.6", 19 | "redux-persist": "^6.0.0", 20 | "reselect": "^4.0.0" 21 | }, 22 | "devDependencies": { 23 | "react-scripts": "^5.0.1" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/redux/directory/directory.reducer.jsx: -------------------------------------------------------------------------------- 1 | const INITIAL_STATE = { 2 | sections: [ 3 | { 4 | title: "hats", 5 | imageUrl: "https://i.ibb.co/cvpntL1/hats.png", 6 | id: 1, 7 | linkUrl: "shop/hats" 8 | }, 9 | { 10 | title: "jackets", 11 | imageUrl: "https://i.ibb.co/px2tCc3/jackets.png", 12 | id: 2, 13 | linkUrl: "shop/jackets" 14 | }, 15 | { 16 | title: "sneakers", 17 | imageUrl: "https://i.ibb.co/0jqHpnp/sneakers.png", 18 | id: 3, 19 | linkUrl: "shop/sneakers" 20 | }, 21 | { 22 | title: "womens", 23 | imageUrl: "https://i.ibb.co/GCCdy8t/womens.png", 24 | size: "large", 25 | id: 4, 26 | linkUrl: "shop/womens" 27 | }, 28 | { 29 | title: "mens", 30 | imageUrl: "https://i.ibb.co/R70vBrQ/men.png", 31 | size: "large", 32 | id: 5, 33 | linkUrl: "shop/mens" 34 | } 35 | ] 36 | } 37 | const directoryReducer = (state = INITIAL_STATE, action) => { 38 | switch(action.type){ 39 | default: 40 | return state 41 | } 42 | } 43 | 44 | export default directoryReducer -------------------------------------------------------------------------------- /src/redux/cart/cart.reducer.js: -------------------------------------------------------------------------------- 1 | import CartActionTypes from './card.types.js'; 2 | import {addItemsToCart, removeItemFromCart} from './cart.utils.js'; 3 | 4 | const INITIAL_STATE = { 5 | hidden: true, 6 | cartItems: [] 7 | } 8 | 9 | const cartReducer = (state = INITIAL_STATE, action) => { 10 | switch (action.type){ 11 | 12 | case CartActionTypes.TOGGLE_CART_HIDDEN: 13 | return { 14 | ...state, 15 | hidden: !state.hidden 16 | } 17 | 18 | case CartActionTypes.ADD_ITEM: 19 | return{ 20 | ...state, 21 | cartItems: addItemsToCart(state.cartItems, action.payload ) 22 | } 23 | 24 | case CartActionTypes.REMOVE_ITEM: 25 | return{ 26 | ...state, 27 | cartItems: removeItemFromCart(state.cartItems, action.payload) 28 | } 29 | 30 | case CartActionTypes.CLEAR_ITEM_FROM_CART: 31 | return{ 32 | ...state, 33 | cartItems: state.cartItems.filter( 34 | cartItem => cartItem.id !== action.payload.id 35 | ) 36 | } 37 | 38 | default: 39 | return state; 40 | } 41 | } 42 | 43 | export default cartReducer; -------------------------------------------------------------------------------- /src/components/menu-item/menu-item.styles.scss: -------------------------------------------------------------------------------- 1 | .menu-item { 2 | min-width: 30%; 3 | height: 240px; 4 | flex: 1 1 auto; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | border: 1px solid black; 9 | margin: 0 7.5px 15px; 10 | overflow: hidden; 11 | 12 | &:hover { 13 | cursor: pointer; 14 | 15 | & .background-image { 16 | transform: scale(1.1); 17 | transition: transform 6s cubic-bezier(0.25, 0.45, 0.45, 0.95); 18 | } 19 | 20 | & .content { 21 | opacity: 0.9; 22 | } 23 | } 24 | 25 | &.large{ 26 | height: 380px; 27 | } 28 | 29 | &:first-child { 30 | margin-right: 7.5px; 31 | } 32 | 33 | &:last-child { 34 | margin-left: 7.5px; 35 | } 36 | 37 | .background-image{ 38 | width: 100%; 39 | height: 100%; 40 | background-position: center; 41 | background-size: cover; 42 | } 43 | 44 | .content { 45 | height: 90px; 46 | padding: 0 25px; 47 | display: flex; 48 | flex-direction: column; 49 | align-items: center; 50 | justify-content: center; 51 | border: 1px solid black; 52 | background-color: white; 53 | opacity: 0.7; 54 | position: absolute; 55 | 56 | .title { 57 | font-weight: bold; 58 | margin-bottom: 6px; 59 | font-size: 22px; 60 | color: #4a4a4a; 61 | } 62 | 63 | .subtitle { 64 | font-weight: lighter; 65 | font-size: 16px; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/components/checkout-item/checkout-item.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { clearItemFromCart, addItem, removeItem } from '../../redux/cart/cart.actions.js' 4 | 5 | import './checkout-item.styles.scss'; 6 | // import { removeItemFromCart } from '../../redux/cart/cart.utils.js'; 7 | 8 | const CheckoutItem = ({ cartItem, clearItem, addItem, removeItem }) => { 9 | const { name, imageUrl, price, quantity } = cartItem; 10 | return ( 11 |
12 |
13 | item 14 |
15 | {name} 16 | 17 |
removeItem(cartItem)} >❮
18 | {quantity} 19 |
addItem(cartItem)}>❯
20 |
21 | {price} 22 |
clearItem(cartItem)}> 23 | ✕ 24 |
25 |
26 | ); 27 | } 28 | 29 | const mapDispatchToProps = dispatch => ({ 30 | clearItem: item => dispatch(clearItemFromCart(item)), 31 | addItem: item => dispatch(addItem(item)), 32 | removeItem: item => dispatch(removeItem(item)) 33 | }) 34 | 35 | export default connect(null, mapDispatchToProps)(CheckoutItem) -------------------------------------------------------------------------------- /src/pages/checkout/checkout.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from 'reselect'; 4 | import { selectCartItems, selectCartTotal } from '../../redux/cart/cart.selectors' 5 | import CheckoutItem from '../../components/checkout-item/checkout-item.component' 6 | 7 | import './checkout.styles.scss'; 8 | 9 | const CheckoutPage = ({cartItems, total}) => ( 10 |
11 |
12 |
13 | Product 14 |
15 | 16 |
17 | Description 18 |
19 | 20 |
21 | Quantity 22 |
23 | 24 |
25 | Price 26 |
27 | 28 |
29 | Remove 30 |
31 |
32 | { 33 | cartItems.map(cartItem => 34 | () 35 | ) 36 | } 37 |
38 | TOTAL: ${total} 39 |
40 |
41 | ); 42 | 43 | const mapStateToProps = createStructuredSelector({ 44 | cartItems: selectCartItems, 45 | total: selectCartTotal 46 | }) 47 | 48 | export default connect(mapStateToProps)(CheckoutPage); -------------------------------------------------------------------------------- /src/components/cart-dropdown/cart-dropdown.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from "reselect"; 4 | import { withRouter } from 'react-router-dom'; 5 | 6 | import CartItem from '../cart-item/cart-item.component'; 7 | import CustomButton from "../custom-button/custom-button.component.jsx"; 8 | 9 | import { selectCartItems } from "../../redux/cart/cart.selectors"; 10 | import { toggleCartHidden } from '../../redux/cart/cart.actions'; 11 | 12 | 13 | import './cart-dropdown.styles.scss'; 14 | 15 | const CartDropdown = ({ cartItems, history, dispatch }) => ( 16 |
17 |
18 | { 19 | cartItems.length ? ( 20 | cartItems.map(cartItem => ( 21 | 22 | )) 23 | ):( 24 | Your cart is empty 25 | )} 26 |
27 | { 29 | history.push('/checkout'); 30 | dispatch(toggleCartHidden()) 31 | }}> 32 | GO TO CHECKOUT 33 | 34 |
35 | ) 36 | 37 | const mapStateToProps = createStructuredSelector({ 38 | cartItems: selectCartItems 39 | }) 40 | 41 | export default withRouter(connect(mapStateToProps)(CartDropdown)); -------------------------------------------------------------------------------- /src/firebase/firebase.utils.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app'; 2 | import 'firebase/firestore'; 3 | import 'firebase/auth'; 4 | 5 | const config = { 6 | apiKey: "AIzaSyCZ1veQMvQwwkb1ka0-zVm2nrk7OgiyDmU", 7 | authDomain: "react-ecommerce-caf86.firebaseapp.com", 8 | databaseURL: "https://react-ecommerce-caf86.firebaseio.com", 9 | projectId: "react-ecommerce-caf86", 10 | storageBucket: "react-ecommerce-caf86.appspot.com", 11 | messagingSenderId: "522930649756", 12 | appId: "1:522930649756:web:f062c083dee844c8440b59", 13 | measurementId: "G-M8X1HNCPV0" 14 | }; 15 | 16 | export const createUserProfileDocument = async (userAuth, additionalData) => { 17 | 18 | if(!userAuth) return 19 | 20 | const userRef = firestore.doc(`users/${userAuth.uid}`) 21 | const snapShot = await userRef.get() 22 | 23 | if(snapShot.exists === false){ 24 | const {displayName, email} = userAuth; 25 | const createdAt = new Date(); 26 | 27 | try{ 28 | await userRef.set({ 29 | displayName, 30 | email, 31 | createdAt, 32 | ...additionalData 33 | }) 34 | } catch (error){ 35 | console.log('error creating user', error.message) 36 | } 37 | } 38 | return userRef; 39 | 40 | } 41 | 42 | firebase.initializeApp(config); 43 | 44 | export const auth = firebase.auth(); 45 | export const firestore = firebase.firestore(); 46 | 47 | const provider = new firebase.auth.GoogleAuthProvider(); 48 | provider.setCustomParameters({prompt: 'select_account'}); 49 | export const signInWithGoogle = () => auth.signInWithPopup(provider); 50 | 51 | export default firebase; -------------------------------------------------------------------------------- /src/components/header/header.component.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import { createStructuredSelector } from "reselect"; 5 | 6 | import { auth } from "../../firebase/firebase.utils.js"; 7 | import { ReactComponent as Logo } from "../../assets/crown.svg"; 8 | import CartIcon from "../cart-icon/cart-icon.component"; 9 | import CartDropdown from "../cart-dropdown/cart-dropdown.component.jsx"; 10 | import { selectCartHidden } from "../../redux/cart/cart.selectors.js"; 11 | import { selectCurrentUser } from "../../redux/user/user.selector.js"; 12 | 13 | import "./header.styles.scss"; 14 | 15 | const Header = ({ currentUser, hidden }) => ( 16 |
17 | 18 | 19 | 20 | 21 |
22 | 23 | SHOP 24 | 25 | 26 | 27 | CONTACT 28 | 29 | {currentUser ? ( 30 |
auth.signOut()}> 31 | SIGN OUT 32 |
33 | ) : ( 34 | 35 | SIGN IN 36 | 37 | )} 38 | 39 |
40 | {hidden ? null : } 41 |

© 2021

42 |
43 | ); 44 | 45 | const mapStateToProps = createStructuredSelector({ 46 | currentUser: selectCurrentUser, 47 | hidden: selectCartHidden, 48 | }); 49 | 50 | export default connect(mapStateToProps)(Header); 51 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | 23 | 32 | FamSolar Web Store 33 | 34 | 35 | 36 |
37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/sign-in/sign-in.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import FormInput from '../form-input/form-input.component'; 4 | import CustomButton from '../custom-button/custom-button.component'; 5 | import {auth, signInWithGoogle} from '../../firebase/firebase.utils.js'; 6 | import './sign-in.styles.scss'; 7 | 8 | class SignIn extends React.Component{ 9 | constructor(props){ 10 | super(props); 11 | 12 | this.state ={ 13 | email: '', 14 | password: '' 15 | } 16 | } 17 | 18 | handleSubmit = async event => { 19 | event.preventDefault(); 20 | 21 | const {email, password} = this.state; 22 | 23 | try{ 24 | await auth.signInWithEmailAndPassword(email, password); 25 | this.setState({ email: "", password: "" }); 26 | }catch(error){ 27 | console.log(error) 28 | } 29 | } 30 | 31 | handleChange = event => { 32 | const { value, name } = event.target; 33 | 34 | this.setState({ [name]: value }) 35 | } 36 | 37 | render(){ 38 | return ( 39 |
40 |

I already have an account

41 | Sign in with your email and password 42 | 43 |
44 | 52 | 53 | 61 |
62 | Sign In 63 | Sign in with Google 64 |
65 | 66 |
67 | ); 68 | } 69 | } 70 | 71 | export default SignIn; -------------------------------------------------------------------------------- /src/assets/shopping-bag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 12 | 16 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Switch, Route, Redirect } from 'react-router-dom' 3 | 4 | import HomePage from './pages/homepage/homepage.component' 5 | import ShopPage from './pages/shop/shop.component' 6 | import CheckoutPage from './pages/checkout/checkout.component' 7 | 8 | import SignInAndSignUp from './pages/sign-in-sign-up/sign-in-sign-up.component' 9 | import Header from './components/header/header.component' 10 | 11 | import { createStructuredSelector } from 'reselect'; 12 | import { auth, createUserProfileDocument } from "./firebase/firebase.utils.js"; 13 | import { connect } from 'react-redux'; 14 | import { setCurrentUser } from './redux/user/user.actions'; 15 | import { selectCurrentUser } from './redux/user/user.selector'; 16 | 17 | import "./App.css"; 18 | 19 | class App extends React.Component{ 20 | unsubscribeFromAuth = null 21 | 22 | componentDidMount(){ 23 | const {setCurrentUser} = this.props 24 | 25 | this.unsubscribeFromAuth = auth.onAuthStateChanged(async userAuth => { 26 | if(userAuth){ 27 | const userRef = await createUserProfileDocument(userAuth) 28 | 29 | userRef.onSnapshot(snapShot => { 30 | setCurrentUser({ 31 | id: snapShot.id, 32 | ...snapShot.data() 33 | }) 34 | }); 35 | } 36 | 37 | setCurrentUser(userAuth) 38 | 39 | }) 40 | } 41 | 42 | componentWillUnmount(){ 43 | this.unsubscribeFromAuth() 44 | } 45 | 46 | render(){ 47 | return ( 48 |
49 |
50 | 51 | 52 | 53 | 54 | 58 | this.props.currentUser ? ( 59 | 60 | 61 | ) : ( 62 | 63 | ) 64 | } 65 | /> 66 | 67 |
68 | ) 69 | } 70 | } 71 | 72 | const mapStateToProps = createStructuredSelector({ 73 | currentUser: selectCurrentUser 74 | }) 75 | 76 | const mapDispatchToProps = dispatch =>({ 77 | setCurrentUser: user => dispatch(setCurrentUser(user)) 78 | }) 79 | 80 | export default connect(mapStateToProps, mapDispatchToProps)(App); 81 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `yarn build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /src/components/sign-up/sign-up.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './sign-up.styles.scss'; 3 | 4 | import FormInput from '../form-input/form-input.component.jsx'; 5 | import CustomButton from '../custom-button/custom-button.component.jsx'; 6 | 7 | 8 | import { auth, createUserProfileDocument } from '../../firebase/firebase.utils.js' 9 | 10 | import './sign-up.styles.scss'; 11 | 12 | class SignUp extends React.Component{ 13 | constructor(){ 14 | super(); 15 | 16 | this.state = { 17 | displayName: '', 18 | email: '', 19 | password: '', 20 | confirmPassword: '' 21 | } 22 | } 23 | 24 | handleSubmit = async event => { 25 | event.preventDefault(); 26 | 27 | const { displayName, email, password, confirmPassword } = this.state; 28 | 29 | if(password !== confirmPassword){ 30 | alert("Passwords don't match") 31 | return; 32 | } 33 | 34 | try{ 35 | const {user} = await auth.createUserWithEmailAndPassword( 36 | email, 37 | password 38 | ) 39 | await createUserProfileDocument(user,{ displayName}) 40 | 41 | this.setState({ 42 | displayName: "", 43 | email: "", 44 | password: "", 45 | confirmPassword: "" 46 | }); 47 | 48 | }catch(error){ 49 | console.error(error); 50 | } 51 | } 52 | 53 | handleChange = event => { 54 | 55 | const {name, value} = event.target 56 | this.setState({[name]: value}); 57 | } 58 | 59 | render(){ 60 | const {displayName, email, password, confirmPassword } = this.state 61 | return ( 62 |
63 |

I do not have an account

64 | Sign up with your email and password 65 |
66 | 74 | 75 | 83 | 84 | 92 | 93 | 101 | 102 | SIGN UP 103 | 104 |
105 | ); 106 | } 107 | } 108 | 109 | export default SignUp; -------------------------------------------------------------------------------- /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.0/8 are 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 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/redux/shop/shop.data.js: -------------------------------------------------------------------------------- 1 | const SHOP_DATA = [ 2 | { 3 | id: 1, 4 | title: "Hats", 5 | routeName: "hats", 6 | items: [ 7 | { 8 | id: 1, 9 | name: "Brown Brim", 10 | imageUrl: "https://i.ibb.co/ZYW3VTp/brown-brim.png", 11 | price: 25 12 | }, 13 | { 14 | id: 2, 15 | name: "Blue Beanie", 16 | imageUrl: "https://i.ibb.co/ypkgK0X/blue-beanie.png", 17 | price: 18 18 | }, 19 | { 20 | id: 3, 21 | name: "Brown Cowboy", 22 | imageUrl: "https://i.ibb.co/QdJwgmp/brown-cowboy.png", 23 | price: 35 24 | }, 25 | { 26 | id: 4, 27 | name: "Grey Brim", 28 | imageUrl: "https://i.ibb.co/RjBLWxB/grey-brim.png", 29 | price: 25 30 | }, 31 | { 32 | id: 5, 33 | name: "Green Beanie", 34 | imageUrl: "https://i.ibb.co/YTjW3vF/green-beanie.png", 35 | price: 18 36 | }, 37 | { 38 | id: 6, 39 | name: "Palm Tree Cap", 40 | imageUrl: "https://i.ibb.co/rKBDvJX/palm-tree-cap.png", 41 | price: 14 42 | }, 43 | { 44 | id: 7, 45 | name: "Red Beanie", 46 | imageUrl: "https://i.ibb.co/bLB646Z/red-beanie.png", 47 | price: 18 48 | }, 49 | { 50 | id: 8, 51 | name: "Wolf Cap", 52 | imageUrl: "https://i.ibb.co/1f2nWMM/wolf-cap.png", 53 | price: 14 54 | }, 55 | { 56 | id: 9, 57 | name: "Blue Snapback", 58 | imageUrl: "https://i.ibb.co/X2VJP2W/blue-snapback.png", 59 | price: 16 60 | } 61 | ] 62 | }, 63 | { 64 | id: 2, 65 | title: "Sneakers", 66 | routeName: "sneakers", 67 | items: [ 68 | { 69 | id: 10, 70 | name: "Adidas NMD", 71 | imageUrl: "https://i.ibb.co/0s3pdnc/adidas-nmd.png", 72 | price: 220 73 | }, 74 | { 75 | id: 11, 76 | name: "Adidas Yeezy", 77 | imageUrl: "https://i.ibb.co/dJbG1cT/yeezy.png", 78 | price: 280 79 | }, 80 | { 81 | id: 12, 82 | name: "Black Converse", 83 | imageUrl: "https://i.ibb.co/bPmVXyP/black-converse.png", 84 | price: 110 85 | }, 86 | { 87 | id: 13, 88 | name: "Nike White AirForce", 89 | imageUrl: "https://i.ibb.co/1RcFPk0/white-nike-high-tops.png", 90 | price: 160 91 | }, 92 | { 93 | id: 14, 94 | name: "Nike Red High Tops", 95 | imageUrl: "https://i.ibb.co/QcvzydB/nikes-red.png", 96 | price: 160 97 | }, 98 | { 99 | id: 15, 100 | name: "Nike Brown High Tops", 101 | imageUrl: "https://i.ibb.co/fMTV342/nike-brown.png", 102 | price: 160 103 | }, 104 | { 105 | id: 16, 106 | name: "Air Jordan Limited", 107 | imageUrl: "https://i.ibb.co/w4k6Ws9/nike-funky.png", 108 | price: 190 109 | }, 110 | { 111 | id: 17, 112 | name: "Timberlands", 113 | imageUrl: "https://i.ibb.co/Mhh6wBg/timberlands.png", 114 | price: 200 115 | } 116 | ] 117 | }, 118 | { 119 | id: 3, 120 | title: "Jackets", 121 | routeName: "jackets", 122 | items: [ 123 | { 124 | id: 18, 125 | name: "Black Jean Shearling", 126 | imageUrl: "https://i.ibb.co/XzcwL5s/black-shearling.png", 127 | price: 125 128 | }, 129 | { 130 | id: 19, 131 | name: "Blue Jean Jacket", 132 | imageUrl: "https://i.ibb.co/mJS6vz0/blue-jean-jacket.png", 133 | price: 90 134 | }, 135 | { 136 | id: 20, 137 | name: "Grey Jean Jacket", 138 | imageUrl: "https://i.ibb.co/N71k1ML/grey-jean-jacket.png", 139 | price: 90 140 | }, 141 | { 142 | id: 21, 143 | name: "Brown Shearling", 144 | imageUrl: "https://i.ibb.co/s96FpdP/brown-shearling.png", 145 | price: 165 146 | }, 147 | { 148 | id: 22, 149 | name: "Tan Trench", 150 | imageUrl: "https://i.ibb.co/M6hHc3F/brown-trench.png", 151 | price: 185 152 | } 153 | ] 154 | }, 155 | { 156 | id: 4, 157 | title: "Womens", 158 | routeName: "womens", 159 | items: [ 160 | { 161 | id: 23, 162 | name: "Blue Tanktop", 163 | imageUrl: "https://i.ibb.co/7CQVJNm/blue-tank.png", 164 | price: 25 165 | }, 166 | { 167 | id: 24, 168 | name: "Floral Blouse", 169 | imageUrl: "https://i.ibb.co/4W2DGKm/floral-blouse.png", 170 | price: 20 171 | }, 172 | { 173 | id: 25, 174 | name: "Floral Dress", 175 | imageUrl: "https://i.ibb.co/KV18Ysr/floral-skirt.png", 176 | price: 80 177 | }, 178 | { 179 | id: 26, 180 | name: "Red Dots Dress", 181 | imageUrl: "https://i.ibb.co/N3BN1bh/red-polka-dot-dress.png", 182 | price: 80 183 | }, 184 | { 185 | id: 27, 186 | name: "Striped Sweater", 187 | imageUrl: "https://i.ibb.co/KmSkMbH/striped-sweater.png", 188 | price: 45 189 | }, 190 | { 191 | id: 28, 192 | name: "Yellow Track Suit", 193 | imageUrl: "https://i.ibb.co/v1cvwNf/yellow-track-suit.png", 194 | price: 135 195 | }, 196 | { 197 | id: 29, 198 | name: "White Blouse", 199 | imageUrl: "https://i.ibb.co/qBcrsJg/white-vest.png", 200 | price: 20 201 | } 202 | ] 203 | }, 204 | { 205 | id: 5, 206 | title: "Mens", 207 | routeName: "mens", 208 | items: [ 209 | { 210 | id: 30, 211 | name: "Camo Down Vest", 212 | imageUrl: "https://i.ibb.co/xJS0T3Y/camo-vest.png", 213 | price: 325 214 | }, 215 | { 216 | id: 31, 217 | name: "Floral T-shirt", 218 | imageUrl: "https://i.ibb.co/qMQ75QZ/floral-shirt.png", 219 | price: 20 220 | }, 221 | { 222 | id: 32, 223 | name: "Black & White Longsleeve", 224 | imageUrl: "https://i.ibb.co/55z32tw/long-sleeve.png", 225 | price: 25 226 | }, 227 | { 228 | id: 33, 229 | name: "Pink T-shirt", 230 | imageUrl: "https://i.ibb.co/RvwnBL8/pink-shirt.png", 231 | price: 25 232 | }, 233 | { 234 | id: 34, 235 | name: "Jean Long Sleeve", 236 | imageUrl: "https://i.ibb.co/VpW4x5t/roll-up-jean-shirt.png", 237 | price: 40 238 | }, 239 | { 240 | id: 35, 241 | name: "Burgundy T-shirt", 242 | imageUrl: "https://i.ibb.co/mh3VM1f/polka-dot-shirt.png", 243 | price: 25 244 | } 245 | ] 246 | } 247 | ]; 248 | 249 | export default SHOP_DATA; 250 | 251 | -------------------------------------------------------------------------------- /src/assets/crown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------