├── .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.jsx │ ├── cart-icon │ │ ├── cart-icon.component.jsx │ │ └── cart-icon.styles.jsx │ ├── cart-item │ │ ├── cart-item.component.jsx │ │ └── cart-item.styles.jsx │ ├── checkout-item │ │ ├── checkout-item.component.jsx │ │ └── checkout-item.styles.jsx │ ├── collection-item │ │ ├── collection-item.component.jsx │ │ └── collection-styles.styles.jsx │ ├── collection-preview │ │ ├── collection-preview.component.jsx │ │ └── collection-preview.styles.jsx │ ├── collections-overview │ │ ├── collections-overview.component.jsx │ │ ├── collections-overview.container.jsx │ │ └── collections-overview.styles.jsx │ ├── custom-button │ │ ├── custom-button.component.jsx │ │ └── custom-button.styles.jsx │ ├── directory │ │ ├── directory.component.jsx │ │ └── directory.styles.jsx │ ├── form-input │ │ ├── form-input.component.jsx │ │ └── form-input.styles.jsx │ ├── header │ │ ├── header.component.jsx │ │ └── header.styles.jsx │ ├── menu-item │ │ ├── menu-item.component.jsx │ │ └── menu-item.styles.jsx │ ├── sign-in │ │ ├── sign-in.component.jsx │ │ └── sign-in.styles.jsx │ ├── sign-up │ │ ├── sign-up.component.jsx │ │ └── sign-up.styles.jsx │ ├── stripe-button │ │ └── stripe-button.component.jsx │ └── with-spinner │ │ ├── with-spinner.component.jsx │ │ └── with-spinner.styles.jsx ├── firebase │ └── firebase.utils.js ├── index.css ├── index.js ├── logo.svg ├── pages │ ├── checkout │ │ ├── checkout.component.jsx │ │ └── checkout.styles.jsx │ ├── collection │ │ ├── collection.component.jsx │ │ ├── collection.container.jsx │ │ └── collection.styles.jsx │ ├── homepage │ │ ├── homepage.component.jsx │ │ └── homepage.styles.jsx │ ├── shop │ │ └── shop.component.jsx │ └── sign-in-and-sign-up │ │ ├── sign-in-and-sign-up.component.jsx │ │ └── sign-in-and-sign-up.styles.jsx ├── 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.actions.js │ │ ├── shop.reducer.js │ │ ├── shop.selectors.js │ │ └── shop.types.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 | ## Lesson-28 2 | 3 | In this lesson we are using redux-thunk for asynchronous event handling! We have modified our collections-page collections-overview components to use the container pattern to separate their loading logic out of our shop component, and into their own isolated files. 4 | 5 | # How to fork and clone 6 | 7 | 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! 8 | 9 | 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. 10 | 11 | ![alt text](https://i.ibb.co/1YN7SJ6/Screen-Shot-2019-07-01-at-2-02-40-AM.png "image to fork button") 12 | 13 | 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! 14 | 15 | 16 | # After you fork and clone: 17 | 18 | ## Install dependencies 19 | 20 | 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. 21 | 22 | ## Set your firebase config 23 | 24 | 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. 25 | 26 | ![alt text](https://i.ibb.co/6ywMkBf/Screen-Shot-2019-07-01-at-11-35-02-AM.png "image to firebase config") 27 | 28 | 29 | ## Set your stripe publishable key 30 | 31 | Set the `publishableKey` variable in the `stripe-button.component.jsx` with your own publishable key from the stripe dashboard. 32 | 33 | ![alt text](https://i.ibb.co/djQTmVF/Screen-Shot-2019-07-01-at-2-18-50-AM.png "image to publishable key") 34 | 35 | ## Things to set before you deploy 36 | 37 | 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: 38 | 39 | ## Set to an existing Heroku app 40 | 41 | 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: 42 | 43 | ``` 44 | heroku apps 45 | ``` 46 | 47 | Copy the name of the app you want to connect the project to, then run: 48 | 49 | ``` 50 | heroku git:remote -a 51 | ``` 52 | 53 | And now you'll have your repo connected to the heroku app under the git remote name `heroku`. 54 | 55 | Then skip to the bottom of this article to see what to do next! 56 | 57 | 58 | ## To create a new Heroku app 59 | 60 | Create a new Heroku project by typing in your terminal: 61 | 62 | ``` 63 | heroku create 64 | ``` 65 | 66 | This will create a new Heroku project for you. Then run: 67 | 68 | ``` 69 | git remote -v 70 | ``` 71 | 72 | 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`. 73 | 74 | 75 | ## Deploying to Heroku 76 | 77 | Add the `mars/create-react-app-buildpack` to your heroku project by typing: 78 | 79 | ``` 80 | heroku buildpacks:set mars/create-react-app-buildpack 81 | ``` 82 | 83 | You can then deploy to heroku by running: 84 | 85 | ``` 86 | git push heroku master 87 | ``` 88 | 89 | You will see this warning message if you are pushing to an existing app: 90 | 91 | ``` 92 | ! [rejected] master -> master (fetch first) 93 | error: failed to push some refs to 'https://git.heroku.com/hasura-crwn-clothing.git' 94 | hint: Updates were rejected because the remote contains work that you do 95 | hint: not have locally. This is usually caused by another repository pushing 96 | hint: to the same ref. You may want to first integrate the remote changes 97 | hint: (e.g., 'git pull ...') before pushing again. 98 | hint: See the 'Note about fast-forwards' in 'git push --help' for details. 99 | ``` 100 | 101 | This is because we are pushing to an existing app that was deploying an entirely different repository from what we have now. Simply run: 102 | 103 | ``` 104 | git push heroku master --force 105 | ``` 106 | 107 | This will overwrite the existing Heroku app with our new code. 108 | 109 | 110 | ## Open our Heroku project 111 | 112 | After heroku finishes building our project, we can simply run: 113 | 114 | ``` 115 | heroku open 116 | ``` 117 | 118 | This will open up our browser and take us to our newly deployed Heroku project! 119 | -------------------------------------------------------------------------------- /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": "^17.0.1", 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 | "redux-thunk": "2.3.0", 18 | "reselect": "4.0.0", 19 | "styled-components": "4.2.0" 20 | }, 21 | "devDependencies": { 22 | "react-scripts": "3.0.0" 23 | }, 24 | "resolutions": { 25 | "babel-jest": "24.7.1" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangMYihua/lesson-28/cd165002545f46a581499ea6f865009f584b2276/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 | import { connect } from 'react-redux'; 4 | import { createStructuredSelector } from 'reselect'; 5 | 6 | import './App.css'; 7 | 8 | import HomePage from './pages/homepage/homepage.component'; 9 | import ShopPage from './pages/shop/shop.component'; 10 | import SignInAndSignUpPage from './pages/sign-in-and-sign-up/sign-in-and-sign-up.component'; 11 | import CheckoutPage from './pages/checkout/checkout.component'; 12 | 13 | import Header from './components/header/header.component'; 14 | 15 | import { auth, createUserProfileDocument } from './firebase/firebase.utils'; 16 | 17 | import { setCurrentUser } from './redux/user/user.actions'; 18 | import { selectCurrentUser } from './redux/user/user.selectors'; 19 | 20 | class App extends React.Component { 21 | unsubscribeFromAuth = null; 22 | 23 | componentDidMount() { 24 | const { setCurrentUser } = this.props; 25 | 26 | this.unsubscribeFromAuth = auth.onAuthStateChanged(async userAuth => { 27 | if (userAuth) { 28 | const userRef = await createUserProfileDocument(userAuth); 29 | 30 | userRef.onSnapshot(snapShot => { 31 | setCurrentUser({ 32 | id: snapShot.id, 33 | ...snapShot.data() 34 | }); 35 | }); 36 | } 37 | 38 | setCurrentUser(userAuth); 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 | const mapStateToProps = createStructuredSelector({ 72 | currentUser: selectCurrentUser 73 | }); 74 | 75 | const mapDispatchToProps = dispatch => ({ 76 | setCurrentUser: user => dispatch(setCurrentUser(user)) 77 | }); 78 | 79 | export default connect( 80 | mapStateToProps, 81 | mapDispatchToProps 82 | )(App); 83 | -------------------------------------------------------------------------------- /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 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 { selectCartItems } from '../../redux/cart/cart.selectors'; 8 | import { toggleCartHidden } from '../../redux/cart/cart.actions.js'; 9 | 10 | import { 11 | CartDropdownContainer, 12 | CartDropdownButton, 13 | EmptyMessageContainer, 14 | CartItemsContainer 15 | } from './cart-dropdown.styles'; 16 | 17 | const CartDropdown = ({ cartItems, history, dispatch }) => ( 18 | 19 | 20 | {cartItems.length ? ( 21 | cartItems.map(cartItem => ( 22 | 23 | )) 24 | ) : ( 25 | Your cart is empty 26 | )} 27 | 28 | { 30 | history.push('/checkout'); 31 | dispatch(toggleCartHidden()); 32 | }} 33 | > 34 | GO TO CHECKOUT 35 | 36 | 37 | ); 38 | 39 | const mapStateToProps = createStructuredSelector({ 40 | cartItems: selectCartItems 41 | }); 42 | 43 | export default withRouter(connect(mapStateToProps)(CartDropdown)); 44 | -------------------------------------------------------------------------------- /src/components/cart-dropdown/cart-dropdown.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import CustomButton from '../custom-button/custom-button.component'; 3 | 4 | export const CartDropdownContainer = styled.div` 5 | position: absolute; 6 | width: 240px; 7 | height: 340px; 8 | display: flex; 9 | flex-direction: column; 10 | padding: 20px; 11 | border: 1px solid black; 12 | background-color: white; 13 | top: 90px; 14 | right: 40px; 15 | z-index: 5; 16 | `; 17 | 18 | export const CartDropdownButton = styled(CustomButton)` 19 | margin-top: auto; 20 | `; 21 | 22 | export const EmptyMessageContainer = styled.span` 23 | font-size: 18px; 24 | margin: 50px auto; 25 | `; 26 | 27 | export const CartItemsContainer = styled.div` 28 | height: 240px; 29 | display: flex; 30 | flex-direction: column; 31 | overflow: scroll; 32 | `; 33 | -------------------------------------------------------------------------------- /src/components/cart-icon/cart-icon.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from 'reselect'; 4 | 5 | import { toggleCartHidden } from '../../redux/cart/cart.actions'; 6 | import { selectCartItemsCount } from '../../redux/cart/cart.selectors'; 7 | 8 | import { 9 | CartContainer, 10 | ShoppingIcon, 11 | ItemCountContainer 12 | } from './cart-icon.styles'; 13 | 14 | const CartIcon = ({ toggleCartHidden, itemCount }) => ( 15 | 16 | 17 | {itemCount} 18 | 19 | ); 20 | 21 | const mapDispatchToProps = dispatch => ({ 22 | toggleCartHidden: () => dispatch(toggleCartHidden()) 23 | }); 24 | 25 | const mapStateToProps = createStructuredSelector({ 26 | itemCount: selectCartItemsCount 27 | }); 28 | 29 | export default connect( 30 | mapStateToProps, 31 | mapDispatchToProps 32 | )(CartIcon); 33 | -------------------------------------------------------------------------------- /src/components/cart-icon/cart-icon.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { ReactComponent as ShoppingIconSVG } from '../../assets/shopping-bag.svg'; 4 | 5 | export const CartContainer = styled.div` 6 | width: 45px; 7 | height: 45px; 8 | position: relative; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | cursor: pointer; 13 | `; 14 | 15 | export const ShoppingIcon = styled(ShoppingIconSVG)` 16 | width: 24px; 17 | height: 24px; 18 | `; 19 | 20 | export const ItemCountContainer = styled.span` 21 | position: absolute; 22 | font-size: 10px; 23 | font-weight: bold; 24 | bottom: 12px; 25 | `; 26 | -------------------------------------------------------------------------------- /src/components/cart-item/cart-item.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | CartItemContainer, 5 | ItemDetailsContainer, 6 | CartItemImage 7 | } from './cart-item.styles'; 8 | 9 | const CartItem = ({ item: { imageUrl, price, name, quantity } }) => ( 10 | 11 | 12 | 13 | {name} 14 | 15 | {quantity} x ${price} 16 | 17 | 18 | 19 | ); 20 | 21 | export default CartItem; 22 | -------------------------------------------------------------------------------- /src/components/cart-item/cart-item.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const CartItemContainer = styled.div` 4 | width: 100%; 5 | display: flex; 6 | height: 80px; 7 | margin-bottom: 15px; 8 | `; 9 | 10 | export const CartItemImage = styled.img` 11 | width: 30%; 12 | `; 13 | 14 | export const ItemDetailsContainer = styled.div` 15 | width: 70%; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: flex-start; 19 | justify-content: center; 20 | padding: 10px 20px; 21 | `; 22 | -------------------------------------------------------------------------------- /src/components/checkout-item/checkout-item.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { 5 | clearItemFromCart, 6 | addItem, 7 | removeItem 8 | } from '../../redux/cart/cart.actions'; 9 | 10 | import { 11 | CheckoutItemContainer, 12 | ImageContainer, 13 | TextContainer, 14 | QuantityContainer, 15 | RemoveButtonContainer 16 | } from './checkout-item.styles'; 17 | 18 | const CheckoutItem = ({ cartItem, clearItem, addItem, removeItem }) => { 19 | const { name, imageUrl, price, quantity } = cartItem; 20 | return ( 21 | 22 | 23 | item 24 | 25 | {name} 26 | 27 |
removeItem(cartItem)}>❮
28 | {quantity} 29 |
addItem(cartItem)}>❯
30 |
31 | {price} 32 | clearItem(cartItem)}> 33 | ✕ 34 | 35 |
36 | ); 37 | }; 38 | 39 | const mapDispatchToProps = dispatch => ({ 40 | clearItem: item => dispatch(clearItemFromCart(item)), 41 | addItem: item => dispatch(addItem(item)), 42 | removeItem: item => dispatch(removeItem(item)) 43 | }); 44 | 45 | export default connect( 46 | null, 47 | mapDispatchToProps 48 | )(CheckoutItem); 49 | -------------------------------------------------------------------------------- /src/components/checkout-item/checkout-item.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const CheckoutItemContainer = styled.div` 4 | width: 100%; 5 | display: flex; 6 | min-height: 100px; 7 | border-bottom: 1px solid darkgrey; 8 | padding: 15px 0; 9 | font-size: 20px; 10 | align-items: center; 11 | `; 12 | 13 | export const ImageContainer = styled.div` 14 | width: 23%; 15 | padding-right: 15px; 16 | 17 | img { 18 | width: 100%; 19 | height: 100%; 20 | } 21 | `; 22 | 23 | export const TextContainer = styled.span` 24 | width: 23%; 25 | `; 26 | 27 | export const QuantityContainer = styled(TextContainer)` 28 | display: flex; 29 | 30 | span { 31 | margin: 0 10px; 32 | } 33 | 34 | div { 35 | cursor: pointer; 36 | } 37 | `; 38 | 39 | export const RemoveButtonContainer = styled.div` 40 | padding-left: 12px; 41 | cursor: pointer; 42 | `; 43 | -------------------------------------------------------------------------------- /src/components/collection-item/collection-item.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { addItem } from '../../redux/cart/cart.actions'; 5 | 6 | import { 7 | CollectionItemContainer, 8 | CollectionFooterContainer, 9 | AddButton, 10 | BackgroundImage, 11 | NameContainer, 12 | PriceContainer 13 | } from './collection-styles.styles'; 14 | 15 | const CollectionItem = ({ item, addItem }) => { 16 | const { name, price, imageUrl } = item; 17 | 18 | return ( 19 | 20 | 21 | 22 | {name} 23 | {price} 24 | 25 | addItem(item)} inverted> 26 | Add to cart 27 | 28 | 29 | ); 30 | }; 31 | 32 | const mapDispatchToProps = dispatch => ({ 33 | addItem: item => dispatch(addItem(item)) 34 | }); 35 | 36 | export default connect( 37 | null, 38 | mapDispatchToProps 39 | )(CollectionItem); 40 | -------------------------------------------------------------------------------- /src/components/collection-item/collection-styles.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import CustomButton from '../custom-button/custom-button.component'; 3 | 4 | export const CollectionItemContainer = styled.div` 5 | width: 22vw; 6 | display: flex; 7 | flex-direction: column; 8 | height: 350px; 9 | align-items: center; 10 | position: relative; 11 | 12 | &:hover { 13 | .image { 14 | opacity: 0.8; 15 | } 16 | 17 | button { 18 | opacity: 0.85; 19 | display: flex; 20 | } 21 | } 22 | `; 23 | 24 | export const AddButton = styled(CustomButton)` 25 | width: 80%; 26 | opacity: 0.7; 27 | position: absolute; 28 | top: 255px; 29 | display: none; 30 | `; 31 | 32 | export const BackgroundImage = styled.div` 33 | width: 100%; 34 | height: 95%; 35 | background-size: cover; 36 | background-position: center; 37 | margin-bottom: 5px; 38 | background-image: ${({ imageUrl }) => `url(${imageUrl})`}; 39 | `; 40 | 41 | export const CollectionFooterContainer = styled.div` 42 | width: 100%; 43 | height: 5%; 44 | display: flex; 45 | justify-content: space-between; 46 | font-size: 18px; 47 | `; 48 | 49 | export const NameContainer = styled.span` 50 | width: 90%; 51 | margin-bottom: 15px; 52 | `; 53 | 54 | export const PriceContainer = styled.span` 55 | width: 10%; 56 | text-align: right; 57 | `; 58 | -------------------------------------------------------------------------------- /src/components/collection-preview/collection-preview.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | 4 | import CollectionItem from '../collection-item/collection-item.component'; 5 | 6 | import { 7 | CollectionPreviewContainer, 8 | TitleContainer, 9 | PreviewContainer 10 | } from './collection-preview.styles'; 11 | 12 | const CollectionPreview = ({ title, items, history, match, routeName }) => ( 13 | 14 | history.push(`${match.path}/${routeName}`)}> 15 | {title.toUpperCase()} 16 | 17 | 18 | {items 19 | .filter((item, idx) => idx < 4) 20 | .map(item => ( 21 | 22 | ))} 23 | 24 | 25 | ); 26 | 27 | export default withRouter(CollectionPreview); 28 | -------------------------------------------------------------------------------- /src/components/collection-preview/collection-preview.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const CollectionPreviewContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | margin-bottom: 30px; 7 | `; 8 | 9 | export const TitleContainer = styled.h1` 10 | font-size: 28px; 11 | margin-bottom: 25px; 12 | cursor: pointer; 13 | 14 | &:hover { 15 | color: grey; 16 | } 17 | `; 18 | 19 | export const PreviewContainer = styled.div` 20 | display: flex; 21 | justify-content: space-between; 22 | `; 23 | -------------------------------------------------------------------------------- /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 CollectionPreview from '../collection-preview/collection-preview.component'; 6 | 7 | import { selectCollectionsForPreview } from '../../redux/shop/shop.selectors'; 8 | import { CollectionsOverviewContainer } from './collections-overview.styles'; 9 | 10 | const CollectionsOverview = ({ collections }) => ( 11 | 12 | {collections.map(({ id, ...otherCollectionProps }) => ( 13 | 14 | ))} 15 | 16 | ); 17 | 18 | const mapStateToProps = createStructuredSelector({ 19 | collections: selectCollectionsForPreview 20 | }); 21 | 22 | export default connect(mapStateToProps)(CollectionsOverview); 23 | -------------------------------------------------------------------------------- /src/components/collections-overview/collections-overview.container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { createStructuredSelector } from 'reselect'; 3 | import { compose } from 'redux'; 4 | 5 | import { selectIsCollectionFetching } from '../../redux/shop/shop.selectors'; 6 | import WithSpinner from '../with-spinner/with-spinner.component'; 7 | import CollectionsOverview from './collections-overview.component'; 8 | 9 | const mapStateToProps = createStructuredSelector({ 10 | isLoading: selectIsCollectionFetching 11 | }); 12 | 13 | const CollectionsOverviewContainer = compose( 14 | connect(mapStateToProps), 15 | WithSpinner 16 | )(CollectionsOverview); 17 | 18 | export default CollectionsOverviewContainer; 19 | -------------------------------------------------------------------------------- /src/components/collections-overview/collections-overview.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const CollectionsOverviewContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | `; 7 | -------------------------------------------------------------------------------- /src/components/custom-button/custom-button.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { CustomButtonContainer } from './custom-button.styles'; 4 | 5 | const CustomButton = ({ children, ...props }) => ( 6 | {children} 7 | ); 8 | 9 | export default CustomButton; 10 | -------------------------------------------------------------------------------- /src/components/custom-button/custom-button.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | 3 | const buttonStyles = css` 4 | background-color: black; 5 | color: white; 6 | border: none; 7 | 8 | &:hover { 9 | background-color: white; 10 | color: black; 11 | border: 1px solid black; 12 | } 13 | `; 14 | 15 | const invertedButtonStyles = css` 16 | background-color: white; 17 | color: black; 18 | border: 1px solid black; 19 | 20 | &:hover { 21 | background-color: black; 22 | color: white; 23 | border: none; 24 | } 25 | `; 26 | 27 | const googleSignInStyles = css` 28 | background-color: #4285f4; 29 | color: white; 30 | 31 | &:hover { 32 | background-color: #357ae8; 33 | border: none; 34 | } 35 | `; 36 | 37 | const getButtonStyles = props => { 38 | if (props.isGoogleSignIn) { 39 | return googleSignInStyles; 40 | } 41 | 42 | return props.inverted ? invertedButtonStyles : buttonStyles; 43 | }; 44 | 45 | export const CustomButtonContainer = styled.button` 46 | min-width: 165px; 47 | width: auto; 48 | height: 50px; 49 | letter-spacing: 0.5px; 50 | line-height: 50px; 51 | padding: 0 35px 0 35px; 52 | font-size: 15px; 53 | text-transform: uppercase; 54 | font-family: 'Open Sans Condensed'; 55 | font-weight: bolder; 56 | cursor: pointer; 57 | display: flex; 58 | justify-content: center; 59 | 60 | ${getButtonStyles} 61 | `; 62 | -------------------------------------------------------------------------------- /src/components/directory/directory.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from 'reselect'; 4 | 5 | import { selectDirectorySections } from '../../redux/directory/directory.selectors'; 6 | 7 | import MenuItem from '../menu-item/menu-item.component'; 8 | 9 | import { DirectoryMenuContainer } from './directory.styles'; 10 | 11 | const Directory = ({ sections }) => ( 12 | 13 | {sections.map(({ id, ...otherSectionProps }) => ( 14 | 15 | ))} 16 | 17 | ); 18 | 19 | const mapStateToProps = createStructuredSelector({ 20 | sections: selectDirectorySections 21 | }); 22 | 23 | export default connect(mapStateToProps)(Directory); 24 | -------------------------------------------------------------------------------- /src/components/directory/directory.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const DirectoryMenuContainer = styled.div` 4 | width: 100%; 5 | display: flex; 6 | flex-wrap: wrap; 7 | justify-content: space-between; 8 | `; 9 | -------------------------------------------------------------------------------- /src/components/form-input/form-input.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | GroupContainer, 5 | FormInputContainer, 6 | FormInputLabel 7 | } from './form-input.styles'; 8 | 9 | const FormInput = ({ handleChange, label, ...props }) => ( 10 | 11 | 12 | {label ? ( 13 | 14 | {label} 15 | 16 | ) : null} 17 | 18 | ); 19 | 20 | export default FormInput; 21 | -------------------------------------------------------------------------------- /src/components/form-input/form-input.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | 3 | const subColor = 'grey'; 4 | const mainColor = 'black'; 5 | 6 | const shrinkLabelStyles = css` 7 | top: -14px; 8 | font-size: 12px; 9 | color: ${mainColor}; 10 | `; 11 | 12 | export const GroupContainer = styled.div` 13 | position: relative; 14 | margin: 45px 0; 15 | 16 | input[type='password'] { 17 | letter-spacing: 0.3em; 18 | } 19 | `; 20 | 21 | export const FormInputContainer = styled.input` 22 | background: none; 23 | background-color: white; 24 | color: ${subColor}; 25 | font-size: 18px; 26 | padding: 10px 10px 10px 5px; 27 | display: block; 28 | width: 100%; 29 | border: none; 30 | border-radius: 0; 31 | border-bottom: 1px solid ${subColor}; 32 | margin: 25px 0; 33 | 34 | &:focus { 35 | outline: none; 36 | } 37 | 38 | &:focus ~ label { 39 | ${shrinkLabelStyles} 40 | } 41 | `; 42 | 43 | export const FormInputLabel = styled.label` 44 | color: ${subColor}; 45 | font-size: 16px; 46 | font-weight: normal; 47 | position: absolute; 48 | pointer-events: none; 49 | left: 5px; 50 | top: 10px; 51 | transition: 300ms ease all; 52 | 53 | &.shrink { 54 | ${shrinkLabelStyles} 55 | } 56 | `; 57 | -------------------------------------------------------------------------------- /src/components/header/header.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from 'reselect'; 4 | 5 | import { auth } from '../../firebase/firebase.utils'; 6 | import CartIcon from '../cart-icon/cart-icon.component'; 7 | import CartDropdown from '../cart-dropdown/cart-dropdown.component'; 8 | import { selectCartHidden } from '../../redux/cart/cart.selectors'; 9 | import { selectCurrentUser } from '../../redux/user/user.selectors'; 10 | 11 | import { ReactComponent as Logo } from '../../assets/crown.svg'; 12 | 13 | import { 14 | HeaderContainer, 15 | LogoContainer, 16 | OptionsContainer, 17 | OptionLink 18 | } from './header.styles'; 19 | 20 | const Header = ({ currentUser, hidden }) => ( 21 | 22 | 23 | 24 | 25 | 26 | SHOP 27 | CONTACT 28 | {currentUser ? ( 29 | auth.signOut()}> 30 | SIGN OUT 31 | 32 | ) : ( 33 | SIGN IN 34 | )} 35 | 36 | 37 | {hidden ? null : } 38 | 39 | ); 40 | 41 | const mapStateToProps = createStructuredSelector({ 42 | currentUser: selectCurrentUser, 43 | hidden: selectCartHidden 44 | }); 45 | 46 | export default connect(mapStateToProps)(Header); 47 | -------------------------------------------------------------------------------- /src/components/header/header.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export const HeaderContainer = styled.div` 5 | height: 70px; 6 | width: 100%; 7 | display: flex; 8 | justify-content: space-between; 9 | margin-bottom: 25px; 10 | `; 11 | 12 | export const LogoContainer = styled(Link)` 13 | height: 100%; 14 | width: 70px; 15 | padding: 25px; 16 | `; 17 | 18 | export const OptionsContainer = styled.div` 19 | width: 50%; 20 | height: 100%; 21 | display: flex; 22 | align-items: center; 23 | justify-content: flex-end; 24 | `; 25 | 26 | export const OptionLink = styled(Link)` 27 | padding: 10px 15px; 28 | cursor: pointer; 29 | `; 30 | -------------------------------------------------------------------------------- /src/components/menu-item/menu-item.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | 4 | import { 5 | MenuItemContainer, 6 | BackgroundImageContainer, 7 | ContentContainer, 8 | ContentTitle, 9 | ContentSubtitle 10 | } from './menu-item.styles'; 11 | 12 | const MenuItem = ({ title, imageUrl, size, history, linkUrl, match }) => ( 13 | history.push(`${match.url}${linkUrl}`)} 16 | > 17 | 21 | 22 | {title.toUpperCase()} 23 | SHOP NOW 24 | 25 | 26 | ); 27 | 28 | export default withRouter(MenuItem); 29 | -------------------------------------------------------------------------------- /src/components/menu-item/menu-item.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const MenuItemContainer = styled.div` 4 | height: ${({ size }) => (size ? '380px' : '240px')}; 5 | min-width: 30%; 6 | overflow: hidden; 7 | flex: 1 1 auto; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | border: 1px solid black; 12 | margin: 0 7.5px 15px; 13 | overflow: hidden; 14 | 15 | &:hover { 16 | cursor: pointer; 17 | 18 | & .background-image { 19 | transform: scale(1.1); 20 | transition: transform 6s cubic-bezier(0.25, 0.45, 0.45, 0.95); 21 | } 22 | 23 | & .content { 24 | opacity: 0.9; 25 | } 26 | } 27 | 28 | &:first-child { 29 | margin-right: 7.5px; 30 | } 31 | 32 | &:last-child { 33 | margin-left: 7.5px; 34 | } 35 | `; 36 | 37 | export const BackgroundImageContainer = styled.div` 38 | width: 100%; 39 | height: 100%; 40 | background-size: cover; 41 | background-position: center; 42 | background-image: ${({ imageUrl }) => `url(${imageUrl})`}; 43 | `; 44 | 45 | export const ContentContainer = styled.div` 46 | height: 90px; 47 | padding: 0 25px; 48 | display: flex; 49 | flex-direction: column; 50 | align-items: center; 51 | justify-content: center; 52 | border: 1px solid black; 53 | background-color: white; 54 | opacity: 0.7; 55 | position: absolute; 56 | `; 57 | 58 | export const ContentTitle = styled.span` 59 | font-weight: bold; 60 | margin-bottom: 6px; 61 | font-size: 22px; 62 | color: #4a4a4a; 63 | `; 64 | 65 | export const ContentSubtitle = styled.span` 66 | font-weight: lighter; 67 | font-size: 16px; 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 { 9 | SignInContainer, 10 | SignInTitle, 11 | ButtonsBarContainer 12 | } from './sign-in.styles'; 13 | 14 | class SignIn extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.state = { 19 | email: '', 20 | password: '' 21 | }; 22 | } 23 | 24 | handleSubmit = async event => { 25 | event.preventDefault(); 26 | 27 | const { email, password } = this.state; 28 | 29 | try { 30 | await auth.signInWithEmailAndPassword(email, password); 31 | this.setState({ email: '', password: '' }); 32 | } catch (error) { 33 | console.log(error); 34 | } 35 | }; 36 | 37 | handleChange = event => { 38 | const { value, name } = event.target; 39 | 40 | this.setState({ [name]: value }); 41 | }; 42 | 43 | render() { 44 | return ( 45 | 46 | I already have an account 47 | Sign in with your email and password 48 | 49 |
50 | 58 | 66 | 67 | Sign in 68 | 69 | Sign in with Google 70 | 71 | 72 | 73 |
74 | ); 75 | } 76 | } 77 | 78 | export default SignIn; 79 | -------------------------------------------------------------------------------- /src/components/sign-in/sign-in.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const SignInContainer = styled.div` 4 | width: 380px; 5 | display: flex; 6 | flex-direction: column; 7 | `; 8 | 9 | export const SignInTitle = styled.h2` 10 | margin: 10px 0; 11 | `; 12 | 13 | export const ButtonsBarContainer = styled.div` 14 | display: flex; 15 | justify-content: space-between; 16 | `; 17 | -------------------------------------------------------------------------------- /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 { SignUpContainer, SignUpTitle } from './sign-up.styles'; 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.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const SignUpContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | width: 380px; 7 | `; 8 | 9 | export const SignUpTitle = styled.h2` 10 | margin: 10px 0; 11 | `; 12 | -------------------------------------------------------------------------------- /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/components/with-spinner/with-spinner.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SpinnerContainer, SpinnerOverlay } from './with-spinner.styles'; 4 | 5 | const WithSpinner = WrappedComponent => { 6 | const Spinner = ({ isLoading, ...otherProps }) => { 7 | return isLoading ? ( 8 | 9 | 10 | 11 | ) : ( 12 | 13 | ); 14 | }; 15 | return Spinner; 16 | }; 17 | 18 | export default WithSpinner; 19 | -------------------------------------------------------------------------------- /src/components/with-spinner/with-spinner.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const SpinnerOverlay = styled.div` 4 | height: 60vh; 5 | width: 100%; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | `; 10 | 11 | export const SpinnerContainer = styled.div` 12 | display: inline-block; 13 | width: 50px; 14 | height: 50px; 15 | border: 3px solid rgba(195, 195, 195, 0.6); 16 | border-radius: 50%; 17 | border-top-color: #636767; 18 | animation: spin 1s ease-in-out infinite; 19 | -webkit-animation: spin 1s ease-in-out infinite; 20 | 21 | @keyframes spin { 22 | to { 23 | -webkit-transform: rotate(360deg); 24 | } 25 | } 26 | @-webkit-keyframes spin { 27 | to { 28 | -webkit-transform: rotate(360deg); 29 | } 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /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 addCollectionAndDocuments = async ( 43 | collectionKey, 44 | objectsToAdd 45 | ) => { 46 | const collectionRef = firestore.collection(collectionKey); 47 | 48 | const batch = firestore.batch(); 49 | objectsToAdd.forEach(obj => { 50 | const newDocRef = collectionRef.doc(); 51 | batch.set(newDocRef, obj); 52 | }); 53 | 54 | return await batch.commit(); 55 | }; 56 | 57 | export const convertCollectionsSnapshotToMap = collections => { 58 | const transformedCollection = collections.docs.map(doc => { 59 | const { title, items } = doc.data(); 60 | 61 | return { 62 | routeName: encodeURI(title.toLowerCase()), 63 | id: doc.id, 64 | title, 65 | items 66 | }; 67 | }); 68 | 69 | return transformedCollection.reduce((accumulator, collection) => { 70 | accumulator[collection.title.toLowerCase()] = collection; 71 | return accumulator; 72 | }, {}); 73 | }; 74 | 75 | export const auth = firebase.auth(); 76 | export const firestore = firebase.firestore(); 77 | 78 | const provider = new firebase.auth.GoogleAuthProvider(); 79 | provider.setCustomParameters({ prompt: 'select_account' }); 80 | export const signInWithGoogle = () => auth.signInWithPopup(provider); 81 | 82 | export default firebase; 83 | -------------------------------------------------------------------------------- /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 | 9 | import './index.css'; 10 | import App from './App'; 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | , 20 | document.getElementById('root') 21 | ); 22 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/pages/checkout/checkout.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from 'reselect'; 4 | 5 | import StripeCheckoutButton from '../../components/stripe-button/stripe-button.component'; 6 | import CheckoutItem from '../../components/checkout-item/checkout-item.component'; 7 | 8 | import { 9 | selectCartItems, 10 | selectCartTotal 11 | } from '../../redux/cart/cart.selectors'; 12 | 13 | import { 14 | CheckoutPageContainer, 15 | CheckoutHeaderContainer, 16 | HeaderBlockContainer, 17 | TotalContainer, 18 | WarningContainer 19 | } from './checkout.styles'; 20 | 21 | const CheckoutPage = ({ cartItems, total }) => ( 22 | 23 | 24 | 25 | Product 26 | 27 | 28 | Description 29 | 30 | 31 | Quantity 32 | 33 | 34 | Price 35 | 36 | 37 | Remove 38 | 39 | 40 | {cartItems.map(cartItem => ( 41 | 42 | ))} 43 | TOTAL: ${total} 44 | 45 | *Please use the following test credit card for payments* 46 |
47 | 4242 4242 4242 4242 - Exp: 01/20 - CVV: 123 48 |
49 | 50 |
51 | ); 52 | 53 | const mapStateToProps = createStructuredSelector({ 54 | cartItems: selectCartItems, 55 | total: selectCartTotal 56 | }); 57 | 58 | export default connect(mapStateToProps)(CheckoutPage); 59 | -------------------------------------------------------------------------------- /src/pages/checkout/checkout.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const CheckoutPageContainer = styled.div` 4 | width: 55%; 5 | min-height: 90vh; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | margin: 50px auto 0; 10 | 11 | button { 12 | margin-left: auto; 13 | margin-top: 50px; 14 | } 15 | `; 16 | 17 | export const CheckoutHeaderContainer = styled.div` 18 | width: 100%; 19 | height: 40px; 20 | display: flex; 21 | justify-content: space-between; 22 | border-bottom: 1px solid darkgrey; 23 | `; 24 | 25 | export const HeaderBlockContainer = styled.div` 26 | text-transform: capitalize; 27 | width: 23%; 28 | 29 | &:last-child { 30 | width: 8%; 31 | } 32 | `; 33 | 34 | export const TotalContainer = styled.div` 35 | margin-top: 30px; 36 | margin-left: auto; 37 | font-size: 36px; 38 | `; 39 | 40 | export const WarningContainer = styled.div` 41 | text-align: center; 42 | margin-top: 40px; 43 | font-size: 24px; 44 | color: red; 45 | `; 46 | -------------------------------------------------------------------------------- /src/pages/collection/collection.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import CollectionItem from '../../components/collection-item/collection-item.component'; 5 | 6 | import { selectCollection } from '../../redux/shop/shop.selectors'; 7 | 8 | import { 9 | CollectionPageContainer, 10 | CollectionTitle, 11 | CollectionItemsContainer 12 | } from './collection.styles'; 13 | 14 | const CollectionPage = ({ collection }) => { 15 | const { title, items } = collection; 16 | return ( 17 | 18 | {title} 19 | 20 | {items.map(item => ( 21 | 22 | ))} 23 | 24 | 25 | ); 26 | }; 27 | 28 | const mapStateToProps = (state, ownProps) => ({ 29 | collection: selectCollection(ownProps.match.params.collectionId)(state) 30 | }); 31 | 32 | export default connect(mapStateToProps)(CollectionPage); 33 | -------------------------------------------------------------------------------- /src/pages/collection/collection.container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { compose } from 'redux'; 3 | import { createStructuredSelector } from 'reselect'; 4 | 5 | import { selectIsCollectionsLoaded } from '../../redux/shop/shop.selectors'; 6 | import WithSpinner from '../../components/with-spinner/with-spinner.component'; 7 | import CollectionPage from './collection.component'; 8 | 9 | const mapStateToProps = createStructuredSelector({ 10 | isLoading: state => !selectIsCollectionsLoaded(state) 11 | }); 12 | 13 | const CollectionPageContainer = compose( 14 | connect(mapStateToProps), 15 | WithSpinner 16 | )(CollectionPage); 17 | 18 | export default CollectionPageContainer; 19 | -------------------------------------------------------------------------------- /src/pages/collection/collection.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const CollectionPageContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | `; 7 | 8 | export const CollectionTitle = styled.h2` 9 | font-size: 38px; 10 | margin: 0 auto 30px; 11 | `; 12 | 13 | export const CollectionItemsContainer = styled.div` 14 | display: grid; 15 | grid-template-columns: 1fr 1fr 1fr 1fr; 16 | grid-gap: 10px; 17 | 18 | & > div { 19 | margin-bottom: 30px; 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /src/pages/homepage/homepage.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Directory from '../../components/directory/directory.component'; 4 | 5 | import { HomePageContainer } from './homepage.styles'; 6 | 7 | const HomePage = () => ( 8 | 9 | 10 | 11 | ); 12 | 13 | export default HomePage; 14 | -------------------------------------------------------------------------------- /src/pages/homepage/homepage.styles.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const HomePageContainer = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | `; 8 | -------------------------------------------------------------------------------- /src/pages/shop/shop.component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | 5 | import { fetchCollectionsStartAsync } from '../../redux/shop/shop.actions'; 6 | 7 | import CollectionsOverviewContainer from '../../components/collections-overview/collections-overview.container'; 8 | import CollectionPageContainer from '../collection/collection.container'; 9 | 10 | class ShopPage extends React.Component { 11 | componentDidMount() { 12 | const { fetchCollectionsStartAsync } = this.props; 13 | 14 | fetchCollectionsStartAsync(); 15 | } 16 | 17 | render() { 18 | const { match } = this.props; 19 | 20 | return ( 21 |
22 | 27 | 31 |
32 | ); 33 | } 34 | } 35 | 36 | const mapDispatchToProps = dispatch => ({ 37 | fetchCollectionsStartAsync: () => dispatch(fetchCollectionsStartAsync()) 38 | }); 39 | 40 | export default connect( 41 | null, 42 | mapDispatchToProps 43 | )(ShopPage); 44 | -------------------------------------------------------------------------------- /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 { SignInAndSignUpContainer } from './sign-in-and-sign-up.styles'; 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.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const SignInAndSignUpContainer = styled.div` 4 | width: 850px; 5 | display: flex; 6 | justify-content: space-between; 7 | margin: 30px auto; 8 | `; 9 | -------------------------------------------------------------------------------- /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.actions.js: -------------------------------------------------------------------------------- 1 | import ShopActionTypes from './shop.types'; 2 | 3 | import { 4 | firestore, 5 | convertCollectionsSnapshotToMap 6 | } from '../../firebase/firebase.utils'; 7 | 8 | export const fetchCollectionsStart = () => ({ 9 | type: ShopActionTypes.FETCH_COLLECTIONS_START 10 | }); 11 | 12 | export const fetchCollectionsSuccess = collectionsMap => ({ 13 | type: ShopActionTypes.FETCH_COLLECTIONS_SUCCESS, 14 | payload: collectionsMap 15 | }); 16 | 17 | export const fetchCollectionsFailure = errorMessage => ({ 18 | type: ShopActionTypes.FETCH_COLLECTIONS_FAILURE, 19 | payload: errorMessage 20 | }); 21 | 22 | export const fetchCollectionsStartAsync = () => { 23 | return dispatch => { 24 | const collectionRef = firestore.collection('collections'); 25 | dispatch(fetchCollectionsStart()); 26 | 27 | collectionRef 28 | .get() 29 | .then(snapshot => { 30 | const collectionsMap = convertCollectionsSnapshotToMap(snapshot); 31 | dispatch(fetchCollectionsSuccess(collectionsMap)); 32 | }) 33 | .catch(error => dispatch(fetchCollectionsFailure(error.message))); 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/redux/shop/shop.reducer.js: -------------------------------------------------------------------------------- 1 | import ShopActionTypes from './shop.types'; 2 | 3 | const INITIAL_STATE = { 4 | collections: null, 5 | isFetching: false, 6 | errorMessage: undefined 7 | }; 8 | 9 | const shopReducer = (state = INITIAL_STATE, action) => { 10 | switch (action.type) { 11 | case ShopActionTypes.FETCH_COLLECTIONS_START: 12 | return { 13 | ...state, 14 | isFetching: true 15 | }; 16 | case ShopActionTypes.FETCH_COLLECTIONS_SUCCESS: 17 | return { 18 | ...state, 19 | isFetching: false, 20 | collections: action.payload 21 | }; 22 | case ShopActionTypes.FETCH_COLLECTIONS_FAILURE: 23 | return { 24 | ...state, 25 | isFetching: false, 26 | errorMessage: action.payload 27 | }; 28 | default: 29 | return state; 30 | } 31 | }; 32 | 33 | export default shopReducer; 34 | -------------------------------------------------------------------------------- /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 => 13 | collections ? Object.keys(collections).map(key => collections[key]) : [] 14 | ); 15 | 16 | export const selectCollection = collectionUrlParam => 17 | createSelector( 18 | [selectCollections], 19 | collections => (collections ? collections[collectionUrlParam] : null) 20 | ); 21 | 22 | export const selectIsCollectionFetching = createSelector( 23 | [selectShop], 24 | shop => shop.isFetching 25 | ); 26 | 27 | export const selectIsCollectionsLoaded = createSelector( 28 | [selectShop], 29 | shop => !!shop.collections 30 | ); 31 | -------------------------------------------------------------------------------- /src/redux/shop/shop.types.js: -------------------------------------------------------------------------------- 1 | const ShopActionTypes = { 2 | FETCH_COLLECTIONS_START: 'FETCH_COLLECTIONS_START', 3 | FETCH_COLLECTIONS_SUCCESS: 'FETCH_COLLECTIONS_SUCCESS', 4 | FETCH_COLLECTIONS_FAILURE: 'FETCH_COLLECTIONS_FAILURE' 5 | }; 6 | 7 | export default ShopActionTypes; 8 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { persistStore } from 'redux-persist'; 3 | import logger from 'redux-logger'; 4 | import thunk from 'redux-thunk'; 5 | 6 | import rootReducer from './root-reducer'; 7 | 8 | const middlewares = [thunk]; 9 | 10 | if (process.env.NODE_ENV === 'development') { 11 | middlewares.push(logger); 12 | } 13 | 14 | export const store = createStore(rootReducer, applyMiddleware(...middlewares)); 15 | 16 | export const persistor = persistStore(store); 17 | 18 | export default { store, persistStore }; 19 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------