├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.css ├── App.js ├── App.test.js ├── assets │ ├── crown.svg │ └── shopping-bag.svg ├── components │ ├── cart-dropdown │ │ ├── cart-dropdown.component.jsx │ │ └── cart-dropdown.styles.scss │ ├── cart-icon │ │ ├── cart-icon.component.jsx │ │ └── cart-icon.styles.scss │ ├── cart-item │ │ ├── cart-item.component.jsx │ │ └── cart-item.styles.scss │ ├── checkout-item │ │ ├── checkout-item.component.jsx │ │ └── checkout-item.styles.scss │ ├── collection-item │ │ ├── collection-item.component.jsx │ │ └── collection-item.styles.scss │ ├── collection-preview │ │ ├── collection-preview.component.jsx │ │ └── collection-preview.styles.scss │ ├── collections-overview │ │ ├── collections-overview.component.jsx │ │ └── collections-overview.styles.scss │ ├── custom-button │ │ ├── custom-buttom.styles.scss │ │ └── custom-button.component.jsx │ ├── directory │ │ ├── directory.component.jsx │ │ └── directory.styles.scss │ ├── form-input │ │ ├── form-input.component.jsx │ │ └── form-input.styles.scss │ ├── header │ │ ├── header.component.jsx │ │ └── header.styles.scss │ ├── menu-item │ │ ├── menu-item.component.jsx │ │ └── menu-item.styles.scss │ ├── sign-in │ │ ├── sign-in.component.jsx │ │ └── sign-in.styles.scss │ ├── sign-up │ │ ├── sign-up.component.jsx │ │ └── sign-up.styles.scss │ └── stripe-button │ │ └── stripe-button.component.jsx ├── contexts │ ├── collections │ │ ├── collections.context.js │ │ └── shop.data.js │ ├── current-user │ │ └── current-user.context.js │ └── directory │ │ ├── directory.context.js │ │ └── directory.data.js ├── firebase │ └── firebase.utils.js ├── index.css ├── index.js ├── logo.svg ├── pages │ ├── checkout │ │ ├── checkout.component.jsx │ │ └── checkout.styles.scss │ ├── collection │ │ ├── collection.component.jsx │ │ └── collection.styles.scss │ ├── homepage │ │ ├── homepage.component.jsx │ │ └── homepage.styles.scss │ ├── shop │ │ └── shop.component.jsx │ └── sign-in-and-sign-up │ │ ├── sign-in-and-sign-up.component.jsx │ │ └── sign-in-and-sign-up.styles.scss ├── providers │ └── cart │ │ ├── cart.provider.jsx │ │ └── cart.utils.js ├── redux │ ├── cart │ │ ├── cart.actions.js │ │ ├── cart.reducer.js │ │ ├── cart.selectors.js │ │ ├── cart.types.js │ │ └── cart.utils.js │ ├── directory │ │ ├── directory.reducer.js │ │ └── directory.selectors.js │ ├── root-reducer.js │ ├── shop │ │ ├── shop.data.js │ │ ├── shop.reducer.js │ │ └── shop.selectors.js │ ├── store.js │ └── user │ │ ├── user.actions.js │ │ ├── user.reducer.js │ │ ├── user.selectors.js │ │ └── user.types.js └── serviceWorker.js └── yarn.lock /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-context-complete 2 | A complete version of our application converting everything redux over to using the contextAPI 3 | 4 | # How to fork and clone 5 | 6 | One quick note about cloning this project. If you wish to make commits and push the code up after cloning this repo, you should fork the project first. In order to own your own copy of this repository, you have to fork it so you get your own copy on your own profile! 7 | 8 | You can see the fork button in the top right corner of every GitHub project; click it and a copy of the project will be added to your GitHub profile under the same name as the original project. 9 | 10 | ![alt text](https://i.ibb.co/1YN7SJ6/Screen-Shot-2019-07-01-at-2-02-40-AM.png "image to fork button") 11 | 12 | After forking the project, simply clone it the way you would from the new forked project in your own GitHub repository and you can commit and push to it freely! 13 | 14 | 15 | # After you fork and clone: 16 | 17 | ## Install dependencies 18 | 19 | In your terminal after you clone your project down, remember to run either `yarn` or `npm install` to build all the dependencies in the project. 20 | 21 | ## Set your firebase config 22 | 23 | Remember to replace the `config` variable in your `firebase.utils.js` with your own config object from the firebase dashboard! Navigate to the project settings and scroll down to the config code. Copy the object in the code and replace the variable in your cloned code. 24 | 25 | ![alt text](https://i.ibb.co/6ywMkBf/Screen-Shot-2019-07-01-at-11-35-02-AM.png "image to firebase config") 26 | 27 | 28 | ## Set your stripe publishable key 29 | 30 | Set the `publishableKey` variable in the `stripe-button.component.jsx` with your own publishable key from the stripe dashboard. 31 | 32 | ![alt text](https://i.ibb.co/djQTmVF/Screen-Shot-2019-07-01-at-2-18-50-AM.png "image to publishable key") 33 | 34 | ## Things to set before you deploy 35 | 36 | You will also need to connect your existing Heroku app to this new forked and cloned repo, or you have to create a new Heroku app and push to it. A quick refresher on how to do either of these: 37 | 38 | ## Set to an existing Heroku app 39 | 40 | To set to an existing Heroku app you already have deployed, you need to know the name of the app you want to deploy to. To see a list of all the apps you currently have on Heroku: 41 | 42 | ``` 43 | heroku apps 44 | ``` 45 | 46 | Copy the name of the app you want to connect the project to, then run: 47 | 48 | ``` 49 | heroku git:remote -a 50 | ``` 51 | 52 | And now you'll have your repo connected to the heroku app under the git remote name `heroku`. 53 | 54 | Then skip to the bottom of this article to see what to do next! 55 | 56 | 57 | ## To create a new Heroku app 58 | 59 | Create a new Heroku project by typing in your terminal: 60 | 61 | ``` 62 | heroku create 63 | ``` 64 | 65 | This will create a new Heroku project for you. Then run: 66 | 67 | ``` 68 | git remote -v 69 | ``` 70 | 71 | You should see heroku `https://git.heroku.com/` in the list. This means you have successfully connected your project to the newly created Heroku app under the git remote of `heroku`. 72 | 73 | 74 | ## Deploying to Heroku 75 | 76 | Add the `mars/create-react-app-buildpack` to your heroku project by typing: 77 | 78 | ``` 79 | heroku buildpacks:set mars/create-react-app-buildpack 80 | ``` 81 | 82 | You can then deploy to heroku by running: 83 | 84 | ``` 85 | git push heroku master 86 | ``` 87 | 88 | You will see this warning message if you are pushing to an existing app: 89 | 90 | ``` 91 | ! [rejected] master -> master (fetch first) 92 | error: failed to push some refs to 'https://git.heroku.com/hasura-crwn-clothing.git' 93 | hint: Updates were rejected because the remote contains work that you do 94 | hint: not have locally. This is usually caused by another repository pushing 95 | hint: to the same ref. You may want to first integrate the remote changes 96 | hint: (e.g., 'git pull ...') before pushing again. 97 | hint: See the 'Note about fast-forwards' in 'git push --help' for details. 98 | ``` 99 | 100 | This is because we are pushing to an existing app that was deploying an entirely different repository from what we have now. Simply run: 101 | 102 | ``` 103 | git push heroku master --force 104 | ``` 105 | 106 | This will overwrite the existing Heroku app with our new code. 107 | 108 | 109 | ## Open our Heroku project 110 | 111 | After heroku finishes building our project, we can simply run: 112 | 113 | ``` 114 | heroku open 115 | ``` 116 | 117 | This will open up our browser and take us to our newly deployed Heroku project! 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crwn-clothing", 3 | "version": "0.1.0", 4 | "proxy": "http://localhost:5000", 5 | "private": true, 6 | "dependencies": { 7 | "firebase": "6.0.2", 8 | "node-sass": "4.12.0", 9 | "react": "^16.8.6", 10 | "react-dom": "^16.8.6", 11 | "react-redux": "7.0.3", 12 | "react-router-dom": "5.0.0", 13 | "react-stripe-checkout": "2.6.3", 14 | "redux": "4.0.1", 15 | "redux-logger": "3.0.6", 16 | "redux-persist": "5.10.0", 17 | "reselect": "4.0.0" 18 | }, 19 | "devDependencies": { 20 | "react-scripts": "3.0.0" 21 | }, 22 | "resolutions": { 23 | "babel-jest": "24.7.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 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangMYihua/react-context-complete/a2a4023f34ef30b59d43d862148e27653fb85cf8/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 26 | CRWN Clothing 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Open Sans Condensed'; 3 | padding: 20px 40px; 4 | } 5 | 6 | a { 7 | text-decoration: none; 8 | color: black; 9 | } 10 | 11 | * { 12 | box-sizing: border-box; 13 | } 14 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Redirect } from 'react-router-dom'; 3 | 4 | import './App.css'; 5 | 6 | import HomePage from './pages/homepage/homepage.component'; 7 | import ShopPage from './pages/shop/shop.component'; 8 | import SignInAndSignUpPage from './pages/sign-in-and-sign-up/sign-in-and-sign-up.component'; 9 | import CheckoutPage from './pages/checkout/checkout.component'; 10 | 11 | import Header from './components/header/header.component'; 12 | 13 | import { auth, createUserProfileDocument } from './firebase/firebase.utils'; 14 | 15 | import CurrentUserContext from './contexts/current-user/current-user.context'; 16 | 17 | class App extends React.Component { 18 | constructor() { 19 | super(); 20 | 21 | this.state = { 22 | currentUser: null 23 | }; 24 | } 25 | 26 | unsubscribeFromAuth = null; 27 | 28 | componentDidMount() { 29 | this.unsubscribeFromAuth = auth.onAuthStateChanged(async userAuth => { 30 | if (userAuth) { 31 | const userRef = await createUserProfileDocument(userAuth); 32 | 33 | userRef.onSnapshot(snapShot => { 34 | this.setState({ 35 | currentUser: { 36 | id: snapShot.id, 37 | ...snapShot.data() 38 | } 39 | }); 40 | }); 41 | } 42 | 43 | this.setState({ currentUser: userAuth }); 44 | }); 45 | } 46 | 47 | componentWillUnmount() { 48 | this.unsubscribeFromAuth(); 49 | } 50 | 51 | render() { 52 | return ( 53 |
54 | 55 |
56 | 57 | 58 | 59 | 60 | 61 | 65 | this.state.currentUser ? ( 66 | 67 | ) : ( 68 | 69 | ) 70 | } 71 | /> 72 | 73 |
74 | ); 75 | } 76 | } 77 | 78 | export default App; 79 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/assets/crown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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/components/cart-dropdown/cart-dropdown.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | 4 | import CustomButton from '../custom-button/custom-button.component'; 5 | import CartItem from '../cart-item/cart-item.component'; 6 | import { CartContext } from '../../providers/cart/cart.provider'; 7 | 8 | import './cart-dropdown.styles.scss'; 9 | 10 | const CartDropdown = ({ history }) => { 11 | const { cartItems, toggleHidden } = useContext(CartContext); 12 | 13 | return ( 14 |
15 |
16 | {cartItems.length ? ( 17 | cartItems.map(cartItem => ( 18 | 19 | )) 20 | ) : ( 21 | Your cart is empty 22 | )} 23 |
24 | { 26 | history.push('/checkout'); 27 | toggleHidden(); 28 | }} 29 | > 30 | GO TO CHECKOUT 31 | 32 |
33 | ); 34 | }; 35 | 36 | export default withRouter(CartDropdown); 37 | -------------------------------------------------------------------------------- /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 | } 30 | -------------------------------------------------------------------------------- /src/components/cart-icon/cart-icon.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import { CartContext } from '../../providers/cart/cart.provider'; 4 | 5 | import { ReactComponent as ShoppingIcon } from '../../assets/shopping-bag.svg'; 6 | 7 | import './cart-icon.styles.scss'; 8 | 9 | const CartIcon = ({ itemCount }) => { 10 | const { toggleHidden, cartItemsCount } = useContext(CartContext); 11 | return ( 12 |
13 | 14 | {cartItemsCount} 15 |
16 | ); 17 | }; 18 | 19 | export default CartIcon; 20 | -------------------------------------------------------------------------------- /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.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 | item 8 |
9 | {name} 10 | 11 | {quantity} x ${price} 12 | 13 |
14 |
15 | ); 16 | 17 | export default CartItem; 18 | -------------------------------------------------------------------------------- /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 | } 24 | -------------------------------------------------------------------------------- /src/components/checkout-item/checkout-item.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import { CartContext } from '../../providers/cart/cart.provider'; 4 | 5 | import './checkout-item.styles.scss'; 6 | 7 | const CheckoutItem = ({ cartItem }) => { 8 | const { name, imageUrl, price, quantity } = cartItem; 9 | const { addItem, removeItem, clearItemFromCart } = useContext(CartContext); 10 | 11 | return ( 12 |
13 |
14 | item 15 |
16 | {name} 17 | 18 |
removeItem(cartItem)}> 19 | ❮ 20 |
21 | {quantity} 22 |
addItem(cartItem)}> 23 | ❯ 24 |
25 |
26 | {price} 27 |
clearItemFromCart(cartItem)} 30 | > 31 | ✕ 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default CheckoutItem; 38 | -------------------------------------------------------------------------------- /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 | display: flex; 27 | 28 | .arrow { 29 | cursor: pointer; 30 | } 31 | 32 | .value { 33 | margin: 0 10px; 34 | } 35 | } 36 | 37 | .remove-button { 38 | padding-left: 12px; 39 | cursor: pointer; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/collection-item/collection-item.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import CustomButton from '../custom-button/custom-button.component'; 4 | import { CartContext } from '../../providers/cart/cart.provider'; 5 | 6 | import './collection-item.styles.scss'; 7 | 8 | const CollectionItem = ({ item }) => { 9 | const { name, price, imageUrl } = item; 10 | const { addItem } = useContext(CartContext); 11 | 12 | return ( 13 |
14 |
20 |
21 | {name} 22 | {price} 23 |
24 | addItem(item)} inverted> 25 | Add to cart 26 | 27 |
28 | ); 29 | }; 30 | 31 | export default CollectionItem; 32 | -------------------------------------------------------------------------------- /src/components/collection-item/collection-item.styles.scss: -------------------------------------------------------------------------------- 1 | .collection-item { 2 | width: 22vw; 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/collection-preview/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; 21 | -------------------------------------------------------------------------------- /src/components/collection-preview/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 | } 16 | -------------------------------------------------------------------------------- /src/components/collections-overview/collections-overview.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import CollectionPreview from '../collection-preview/collection-preview.component'; 4 | import CollectionsContext from '../../contexts/collections/collections.context'; 5 | 6 | import './collections-overview.styles.scss'; 7 | 8 | const CollectionsOverview = () => { 9 | const collectionsMap = useContext(CollectionsContext); 10 | const collections = Object.keys(collectionsMap).map( 11 | key => collectionsMap[key] 12 | ); 13 | 14 | return ( 15 |
16 | {collections.map(({ id, ...otherCollectionProps }) => ( 17 | 18 | ))} 19 |
20 | ); 21 | }; 22 | 23 | export default CollectionsOverview; 24 | -------------------------------------------------------------------------------- /src/components/collections-overview/collections-overview.styles.scss: -------------------------------------------------------------------------------- 1 | .collections-overview { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/custom-button/custom-buttom.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 | } 47 | -------------------------------------------------------------------------------- /src/components/custom-button/custom-button.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './custom-buttom.styles.scss'; 4 | 5 | const CustomButton = ({ 6 | children, 7 | isGoogleSignIn, 8 | inverted, 9 | ...otherProps 10 | }) => ( 11 | 19 | ); 20 | 21 | export default CustomButton; 22 | -------------------------------------------------------------------------------- /src/components/directory/directory.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import MenuItem from '../menu-item/menu-item.component'; 4 | 5 | import DirectoryContext from '../../contexts/directory/directory.context'; 6 | 7 | import './directory.styles.scss'; 8 | 9 | const Directory = () => { 10 | const sections = useContext(DirectoryContext); 11 | 12 | return ( 13 |
14 | {sections.map(({ id, ...otherSectionProps }) => ( 15 | 16 | ))} 17 |
18 | ); 19 | }; 20 | 21 | export default Directory; 22 | -------------------------------------------------------------------------------- /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 | } 7 | -------------------------------------------------------------------------------- /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; 21 | -------------------------------------------------------------------------------- /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 | } 55 | -------------------------------------------------------------------------------- /src/components/header/header.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { auth } from '../../firebase/firebase.utils'; 5 | import CartIcon from '../cart-icon/cart-icon.component'; 6 | import CartDropdown from '../cart-dropdown/cart-dropdown.component'; 7 | import CurrentUserContext from '../../contexts/current-user/current-user.context'; 8 | import { CartContext } from '../../providers/cart/cart.provider'; 9 | 10 | import { ReactComponent as Logo } from '../../assets/crown.svg'; 11 | 12 | import './header.styles.scss'; 13 | 14 | const Header = () => { 15 | const currentUser = useContext(CurrentUserContext); 16 | const { hidden } = useContext(CartContext); 17 | 18 | return ( 19 |
20 | 21 | 22 | 23 |
24 | 25 | SHOP 26 | 27 | 28 | CONTACT 29 | 30 | {currentUser ? ( 31 |
auth.signOut()}> 32 | SIGN OUT 33 |
34 | ) : ( 35 | 36 | SIGN IN 37 | 38 | )} 39 | 40 |
41 | {hidden ? null : } 42 |
43 | ); 44 | }; 45 | 46 | export default Header; 47 | -------------------------------------------------------------------------------- /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 | 21 | .option { 22 | padding: 10px 15px; 23 | cursor: pointer; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/menu-item/menu-item.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | 4 | import './menu-item.styles.scss'; 5 | 6 | const MenuItem = ({ title, imageUrl, size, history, linkUrl, match }) => ( 7 |
history.push(`${match.url}${linkUrl}`)} 10 | > 11 |
17 |
18 |

{title.toUpperCase()}

19 | SHOP NOW 20 |
21 |
22 | ); 23 | 24 | export default withRouter(MenuItem); 25 | -------------------------------------------------------------------------------- /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-size: cover; 41 | background-position: center; 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: 0 6px 0; 59 | font-size: 22px; 60 | color: #4a4a4a; 61 | } 62 | 63 | .subtitle { 64 | font-weight: lighter; 65 | font-size: 16px; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /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 | 6 | import { auth, signInWithGoogle } from '../../firebase/firebase.utils'; 7 | 8 | import './sign-in.styles.scss'; 9 | 10 | class SignIn extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | email: '', 16 | password: '' 17 | }; 18 | } 19 | 20 | handleSubmit = async event => { 21 | event.preventDefault(); 22 | 23 | const { email, password } = this.state; 24 | 25 | try { 26 | await auth.signInWithEmailAndPassword(email, password); 27 | this.setState({ email: '', password: '' }); 28 | } catch (error) { 29 | console.log(error); 30 | } 31 | }; 32 | 33 | handleChange = event => { 34 | const { value, name } = event.target; 35 | 36 | this.setState({ [name]: value }); 37 | }; 38 | 39 | render() { 40 | return ( 41 |
42 |

I already have an account

43 | Sign in with your email and password 44 | 45 |
46 | 54 | 62 |
63 | Sign in 64 | 65 | Sign in with Google 66 | 67 |
68 | 69 |
70 | ); 71 | } 72 | } 73 | 74 | export default SignIn; 75 | -------------------------------------------------------------------------------- /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 0; 8 | } 9 | 10 | .buttons { 11 | display: flex; 12 | justify-content: space-between; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/sign-up/sign-up.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 | 6 | import { auth, createUserProfileDocument } from '../../firebase/firebase.utils'; 7 | 8 | import './sign-up.styles.scss'; 9 | 10 | class SignUp extends React.Component { 11 | constructor() { 12 | super(); 13 | 14 | this.state = { 15 | displayName: '', 16 | email: '', 17 | password: '', 18 | confirmPassword: '' 19 | }; 20 | } 21 | 22 | handleSubmit = async event => { 23 | event.preventDefault(); 24 | 25 | const { displayName, email, password, confirmPassword } = this.state; 26 | 27 | if (password !== confirmPassword) { 28 | alert("passwords don't match"); 29 | return; 30 | } 31 | 32 | try { 33 | const { user } = await auth.createUserWithEmailAndPassword( 34 | email, 35 | password 36 | ); 37 | 38 | await createUserProfileDocument(user, { displayName }); 39 | 40 | this.setState({ 41 | displayName: '', 42 | email: '', 43 | password: '', 44 | confirmPassword: '' 45 | }); 46 | } catch (error) { 47 | console.error(error); 48 | } 49 | }; 50 | 51 | handleChange = event => { 52 | const { name, value } = event.target; 53 | 54 | this.setState({ [name]: value }); 55 | }; 56 | 57 | render() { 58 | const { displayName, email, password, confirmPassword } = this.state; 59 | return ( 60 |
61 |

I do not have a account

62 | Sign up with your email and password 63 |
64 | 72 | 80 | 88 | 96 | SIGN UP 97 | 98 |
99 | ); 100 | } 101 | } 102 | 103 | export default SignUp; 104 | -------------------------------------------------------------------------------- /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 0; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/stripe-button/stripe-button.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StripeCheckout from 'react-stripe-checkout'; 3 | 4 | const StripeCheckoutButton = ({ price }) => { 5 | const priceForStripe = price * 100; 6 | const publishableKey = 'pk_test_WBqax2FWVzS9QlpJScO07iuL'; 7 | 8 | const onToken = token => { 9 | console.log(token); 10 | alert('Payment Succesful!'); 11 | }; 12 | 13 | return ( 14 | 26 | ); 27 | }; 28 | 29 | export default StripeCheckoutButton; 30 | -------------------------------------------------------------------------------- /src/contexts/collections/collections.context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | import SHOP_DATA from './shop.data'; 4 | 5 | const CollectionsContext = createContext(SHOP_DATA); 6 | 7 | export default CollectionsContext; 8 | -------------------------------------------------------------------------------- /src/contexts/collections/shop.data.js: -------------------------------------------------------------------------------- 1 | const SHOP_DATA = { 2 | hats: { 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 | sneakers: { 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 | jackets: { 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 | womens: { 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 | mens: { 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 | -------------------------------------------------------------------------------- /src/contexts/current-user/current-user.context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const CurrentUserContext = createContext(undefined); 4 | 5 | export default CurrentUserContext; 6 | -------------------------------------------------------------------------------- /src/contexts/directory/directory.context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | import DIRECTORY_DATA from './directory.data'; 4 | 5 | const DirectoryContext = createContext(DIRECTORY_DATA); 6 | 7 | export default DirectoryContext; 8 | -------------------------------------------------------------------------------- /src/contexts/directory/directory.data.js: -------------------------------------------------------------------------------- 1 | const DIRECTORY_DATA = [ 2 | { 3 | title: 'hats', 4 | imageUrl: 'https://i.ibb.co/cvpntL1/hats.png', 5 | id: 1, 6 | linkUrl: 'shop/hats' 7 | }, 8 | { 9 | title: 'jackets', 10 | imageUrl: 'https://i.ibb.co/px2tCc3/jackets.png', 11 | id: 2, 12 | linkUrl: 'shop/jackets' 13 | }, 14 | { 15 | title: 'sneakers', 16 | imageUrl: 'https://i.ibb.co/0jqHpnp/sneakers.png', 17 | id: 3, 18 | linkUrl: 'shop/sneakers' 19 | }, 20 | { 21 | title: 'womens', 22 | imageUrl: 'https://i.ibb.co/GCCdy8t/womens.png', 23 | size: 'large', 24 | id: 4, 25 | linkUrl: 'shop/womens' 26 | }, 27 | { 28 | title: 'mens', 29 | imageUrl: 'https://i.ibb.co/R70vBrQ/men.png', 30 | size: 'large', 31 | id: 5, 32 | linkUrl: 'shop/mens' 33 | } 34 | ]; 35 | 36 | export default DIRECTORY_DATA; 37 | -------------------------------------------------------------------------------- /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: 'AIzaSyCdHT-AYHXjF7wOrfAchX4PIm3cSj5tn14', 7 | authDomain: 'crwn-db.firebaseapp.com', 8 | databaseURL: 'https://crwn-db.firebaseio.com', 9 | projectId: 'crwn-db', 10 | storageBucket: 'crwn-db.appspot.com', 11 | messagingSenderId: '850995411664', 12 | appId: '1:850995411664:web:7ddc01d597846f65' 13 | }; 14 | 15 | firebase.initializeApp(config); 16 | 17 | export const createUserProfileDocument = async (userAuth, additionalData) => { 18 | if (!userAuth) return; 19 | 20 | const userRef = firestore.doc(`users/${userAuth.uid}`); 21 | 22 | const snapShot = await userRef.get(); 23 | 24 | if (!snapShot.exists) { 25 | const { displayName, email } = userAuth; 26 | const createdAt = new Date(); 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 | 39 | return userRef; 40 | }; 41 | 42 | export const auth = firebase.auth(); 43 | export const firestore = firebase.firestore(); 44 | 45 | const provider = new firebase.auth.GoogleAuthProvider(); 46 | provider.setCustomParameters({ prompt: 'select_account' }); 47 | export const signInWithGoogle = () => auth.signInWithPopup(provider); 48 | 49 | export default firebase; 50 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /src/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 | 7 | import { store, persistor } from './redux/store'; 8 | import CartProvider from './providers/cart/cart.provider.jsx'; 9 | 10 | import './index.css'; 11 | import App from './App'; 12 | 13 | ReactDOM.render( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | , 23 | document.getElementById('root') 24 | ); 25 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/pages/checkout/checkout.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import CheckoutItem from '../../components/checkout-item/checkout-item.component'; 4 | import StripeCheckoutButton from '../../components/stripe-button/stripe-button.component'; 5 | 6 | import { CartContext } from '../../providers/cart/cart.provider'; 7 | 8 | import './checkout.styles.scss'; 9 | 10 | const CheckoutPage = () => { 11 | const { cartItems, cartTotal } = useContext(CartContext); 12 | 13 | return ( 14 |
15 |
16 |
17 | Product 18 |
19 |
20 | Description 21 |
22 |
23 | Quantity 24 |
25 |
26 | Price 27 |
28 |
29 | Remove 30 |
31 |
32 | {cartItems.map(cartItem => ( 33 | 34 | ))} 35 |
TOTAL: ${cartTotal}
36 |
37 | *Please use the following test credit card for payments* 38 |
39 | 4242 4242 4242 4242 - Exp: 01/20 - CVV: 123 40 |
41 | 42 |
43 | ); 44 | }; 45 | 46 | export default CheckoutPage; 47 | -------------------------------------------------------------------------------- /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 | 32 | button { 33 | margin-left: auto; 34 | margin-top: 50px; 35 | } 36 | 37 | .test-warning { 38 | text-align: center; 39 | margin-top: 40px; 40 | font-size: 24px; 41 | color: red; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/pages/collection/collection.component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import CollectionItem from '../../components/collection-item/collection-item.component'; 4 | 5 | import CollectionsContext from '../../contexts/collections/collections.context'; 6 | 7 | import './collection.styles.scss'; 8 | 9 | const CollectionPage = ({ match }) => { 10 | const collections = useContext(CollectionsContext); 11 | const collection = collections[match.params.collectionId]; 12 | const { title, items } = collection; 13 | 14 | return ( 15 |
16 |

{title}

17 |
18 | {items.map(item => ( 19 | 20 | ))} 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default CollectionPage; 27 | -------------------------------------------------------------------------------- /src/pages/collection/collection.styles.scss: -------------------------------------------------------------------------------- 1 | .collection-page { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | .title { 6 | font-size: 38px; 7 | margin: 0 auto 30px; 8 | } 9 | 10 | .items { 11 | display: grid; 12 | grid-template-columns: 1fr 1fr 1fr 1fr; 13 | grid-gap: 10px; 14 | 15 | & .collection-item { 16 | margin-bottom: 30px; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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/homepage/homepage.styles.scss: -------------------------------------------------------------------------------- 1 | .homepage { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/shop/shop.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | 4 | import CollectionsOverview from '../../components/collections-overview/collections-overview.component'; 5 | import CollectionPage from '../collection/collection.component'; 6 | 7 | const ShopPage = ({ match }) => ( 8 |
9 | 10 | 11 |
12 | ); 13 | 14 | export default ShopPage; 15 | -------------------------------------------------------------------------------- /src/pages/sign-in-and-sign-up/sign-in-and-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-and-sign-up.styles.scss'; 7 | 8 | const SignInAndSignUpPage = () => ( 9 |
10 | 11 | 12 |
13 | ); 14 | 15 | export default SignInAndSignUpPage; 16 | -------------------------------------------------------------------------------- /src/pages/sign-in-and-sign-up/sign-in-and-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 | } 7 | -------------------------------------------------------------------------------- /src/providers/cart/cart.provider.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useEffect } from 'react'; 2 | 3 | import { 4 | addItemToCart, 5 | removeItemFromCart, 6 | filterItemFromCart, 7 | getCartItemsCount, 8 | getCartTotal 9 | } from './cart.utils'; 10 | 11 | export const CartContext = createContext({ 12 | hidden: true, 13 | toggleHidden: () => {}, 14 | cartItems: [], 15 | addItem: () => {}, 16 | removeItem: () => {}, 17 | clearItemFromCart: () => {}, 18 | cartItemsCount: 0, 19 | cartTotal: 0 20 | }); 21 | 22 | const CartProvider = ({ children }) => { 23 | const [hidden, setHidden] = useState(true); 24 | const [cartItems, setCartItems] = useState([]); 25 | const [cartItemsCount, setCartItemsCount] = useState(0); 26 | const [cartTotal, setCartTotal] = useState(0); 27 | 28 | const addItem = item => setCartItems(addItemToCart(cartItems, item)); 29 | const removeItem = item => setCartItems(removeItemFromCart(cartItems, item)); 30 | const toggleHidden = () => setHidden(!hidden); 31 | const clearItemFromCart = item => 32 | setCartItems(filterItemFromCart(cartItems, item)); 33 | 34 | useEffect(() => { 35 | setCartItemsCount(getCartItemsCount(cartItems)); 36 | setCartTotal(getCartTotal(cartItems)); 37 | }, [cartItems]); 38 | 39 | return ( 40 | 52 | {children} 53 | 54 | ); 55 | }; 56 | 57 | export default CartProvider; 58 | -------------------------------------------------------------------------------- /src/providers/cart/cart.utils.js: -------------------------------------------------------------------------------- 1 | export const addItemToCart = (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 | }; 32 | 33 | export const filterItemFromCart = (cartItems, item) => 34 | cartItems.filter(cartItem => cartItem.id !== item.id); 35 | 36 | export const getCartItemsCount = cartItems => 37 | cartItems.reduce( 38 | (accumalatedQuantity, cartItem) => accumalatedQuantity + cartItem.quantity, 39 | 0 40 | ); 41 | 42 | export const getCartTotal = cartItems => 43 | cartItems.reduce( 44 | (accumalatedQuantity, cartItem) => 45 | accumalatedQuantity + cartItem.quantity * cartItem.price, 46 | 0 47 | ); 48 | -------------------------------------------------------------------------------- /src/redux/cart/cart.actions.js: -------------------------------------------------------------------------------- 1 | import CartActionTypes from './cart.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/cart/cart.reducer.js: -------------------------------------------------------------------------------- 1 | import CartActionTypes from './cart.types'; 2 | import { addItemToCart, removeItemFromCart } from './cart.utils'; 3 | 4 | const INITIAL_STATE = { 5 | hidden: true, 6 | cartItems: [] 7 | }; 8 | 9 | const cartReducer = (state = INITIAL_STATE, action) => { 10 | switch (action.type) { 11 | case CartActionTypes.TOGGLE_CART_HIDDEN: 12 | return { 13 | ...state, 14 | hidden: !state.hidden 15 | }; 16 | case CartActionTypes.ADD_ITEM: 17 | return { 18 | ...state, 19 | cartItems: addItemToCart(state.cartItems, action.payload) 20 | }; 21 | case CartActionTypes.REMOVE_ITEM: 22 | return { 23 | ...state, 24 | cartItems: removeItemFromCart(state.cartItems, action.payload) 25 | }; 26 | case CartActionTypes.CLEAR_ITEM_FROM_CART: 27 | return { 28 | ...state, 29 | cartItems: state.cartItems.filter( 30 | cartItem => cartItem.id !== action.payload.id 31 | ) 32 | }; 33 | default: 34 | return state; 35 | } 36 | }; 37 | 38 | export default cartReducer; 39 | -------------------------------------------------------------------------------- /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 | (accumalatedQuantity, cartItem) => 20 | accumalatedQuantity + cartItem.quantity, 21 | 0 22 | ) 23 | ); 24 | 25 | export const selectCartTotal = createSelector( 26 | [selectCartItems], 27 | cartItems => 28 | cartItems.reduce( 29 | (accumalatedQuantity, cartItem) => 30 | accumalatedQuantity + cartItem.quantity * cartItem.price, 31 | 0 32 | ) 33 | ); 34 | -------------------------------------------------------------------------------- /src/redux/cart/cart.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/cart/cart.utils.js: -------------------------------------------------------------------------------- 1 | export const addItemToCart = (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 | }; 32 | -------------------------------------------------------------------------------- /src/redux/directory/directory.reducer.js: -------------------------------------------------------------------------------- 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 | 38 | const directoryReducer = (state = INITIAL_STATE, action) => { 39 | switch (action.type) { 40 | default: 41 | return state; 42 | } 43 | }; 44 | 45 | export default directoryReducer; 46 | -------------------------------------------------------------------------------- /src/redux/directory/directory.selectors.js: -------------------------------------------------------------------------------- 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 | ); 9 | -------------------------------------------------------------------------------- /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'; 6 | import cartReducer from './cart/cart.reducer'; 7 | import directoryReducer from './directory/directory.reducer'; 8 | import shopReducer from './shop/shop.reducer'; 9 | 10 | const persistConfig = { 11 | key: 'root', 12 | storage, 13 | whitelist: ['cart'] 14 | }; 15 | 16 | const rootReducer = combineReducers({ 17 | user: userReducer, 18 | cart: cartReducer, 19 | directory: directoryReducer, 20 | shop: shopReducer 21 | }); 22 | 23 | export default persistReducer(persistConfig, rootReducer); 24 | -------------------------------------------------------------------------------- /src/redux/shop/shop.data.js: -------------------------------------------------------------------------------- 1 | const SHOP_DATA = { 2 | hats: { 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 | sneakers: { 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 | jackets: { 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 | womens: { 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 | mens: { 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 | -------------------------------------------------------------------------------- /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; 15 | -------------------------------------------------------------------------------- /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 | 10 | export const selectCollectionsForPreview = createSelector( 11 | [selectCollections], 12 | collections => Object.keys(collections).map(key => collections[key]) 13 | ); 14 | 15 | export const selectCollection = collectionUrlParam => 16 | createSelector( 17 | [selectCollections], 18 | collections => collections[collectionUrlParam] 19 | ); 20 | -------------------------------------------------------------------------------- /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 | 9 | if (process.env.NODE_ENV === 'development') { 10 | middlewares.push(logger); 11 | } 12 | 13 | export const store = createStore(rootReducer, applyMiddleware(...middlewares)); 14 | 15 | export const persistor = persistStore(store); 16 | 17 | export default { store, persistStore }; 18 | -------------------------------------------------------------------------------- /src/redux/user/user.actions.js: -------------------------------------------------------------------------------- 1 | import { UserActionTypes } from './user.types'; 2 | 3 | export const setCurrentUser = user => ({ 4 | type: UserActionTypes.SET_CURRENT_USER, 5 | payload: user 6 | }); 7 | -------------------------------------------------------------------------------- /src/redux/user/user.reducer.js: -------------------------------------------------------------------------------- 1 | import { UserActionTypes } from './user.types'; 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; 20 | -------------------------------------------------------------------------------- /src/redux/user/user.selectors.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 | -------------------------------------------------------------------------------- /src/redux/user/user.types.js: -------------------------------------------------------------------------------- 1 | export const UserActionTypes = { 2 | SET_CURRENT_USER: 'SET_CURRENT_USER' 3 | }; 4 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | --------------------------------------------------------------------------------