├── 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 |
16 | {children}
17 |
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 |
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 |
14 | {label}
15 |
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 |
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 | You need to enable JavaScript to run this app.
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 |
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 |
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 |
--------------------------------------------------------------------------------