├── .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 ├── 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 ├── 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 | ## Lesson-23 2 | 3 | We have now created our collection page and converted our shop items data over from an array to an object to better leverage our url parameters! Converting arrays over to objects to store data is called data-normalization and it makes searching for specific elements in our code much easier and efficient! 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crwn-clothing", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "firebase": "6.0.2", 7 | "node-sass": "4.12.0", 8 | "react": "^17.0.1", 9 | "react-dom": "^16.8.6", 10 | "react-redux": "7.0.3", 11 | "react-router-dom": "5.0.0", 12 | "redux": "4.0.1", 13 | "redux-logger": "3.0.6", 14 | "redux-persist": "5.10.0", 15 | "reselect": "4.0.0" 16 | }, 17 | "devDependencies": { 18 | "react-scripts": "3.0.0" 19 | }, 20 | "resolutions": { 21 | "babel-jest": "24.7.1" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhangMYihua/lesson-23/cff68611de9d14dcfba15b21a10628871a8f5c13/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 26 | React App 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 CustomButton from '../custom-button/custom-button.component'; 7 | import CartItem from '../cart-item/cart-item.component'; 8 | import { selectCartItems } from '../../redux/cart/cart.selectors'; 9 | import { toggleCartHidden } from '../../redux/cart/cart.actions.js'; 10 | 11 | import './cart-dropdown.styles.scss'; 12 | 13 | const CartDropdown = ({ cartItems, history, dispatch }) => ( 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 | dispatch(toggleCartHidden()); 28 | }} 29 | > 30 | GO TO CHECKOUT 31 | 32 |
33 | ); 34 | 35 | const mapStateToProps = createStructuredSelector({ 36 | cartItems: selectCartItems 37 | }); 38 | 39 | export default withRouter(connect(mapStateToProps)(CartDropdown)); 40 | -------------------------------------------------------------------------------- /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 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 { ReactComponent as ShoppingIcon } from '../../assets/shopping-bag.svg'; 9 | 10 | import './cart-icon.styles.scss'; 11 | 12 | const CartIcon = ({ toggleCartHidden, itemCount }) => ( 13 |
14 | 15 | {itemCount} 16 |
17 | ); 18 | 19 | const mapDispatchToProps = dispatch => ({ 20 | toggleCartHidden: () => dispatch(toggleCartHidden()) 21 | }); 22 | 23 | const mapStateToProps = createStructuredSelector({ 24 | itemCount: selectCartItemsCount 25 | }); 26 | 27 | export default connect( 28 | mapStateToProps, 29 | mapDispatchToProps 30 | )(CartIcon); 31 | -------------------------------------------------------------------------------- /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 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 './checkout-item.styles.scss'; 11 | 12 | const CheckoutItem = ({ cartItem, clearItem, addItem, removeItem }) => { 13 | const { name, imageUrl, price, quantity } = cartItem; 14 | return ( 15 |
16 |
17 | item 18 |
19 | {name} 20 | 21 |
removeItem(cartItem)}> 22 | ❮ 23 |
24 | {quantity} 25 |
addItem(cartItem)}> 26 | ❯ 27 |
28 |
29 | {price} 30 |
clearItem(cartItem)}> 31 | ✕ 32 |
33 |
34 | ); 35 | }; 36 | 37 | const mapDispatchToProps = dispatch => ({ 38 | clearItem: item => dispatch(clearItemFromCart(item)), 39 | addItem: item => dispatch(addItem(item)), 40 | removeItem: item => dispatch(removeItem(item)) 41 | }); 42 | 43 | export default connect( 44 | null, 45 | mapDispatchToProps 46 | )(CheckoutItem); 47 | -------------------------------------------------------------------------------- /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 from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import CustomButton from '../custom-button/custom-button.component'; 5 | import { addItem } from '../../redux/cart/cart.actions'; 6 | 7 | import './collection-item.styles.scss'; 8 | 9 | const CollectionItem = ({ item, addItem }) => { 10 | const { name, price, imageUrl } = item; 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 | const mapDispatchToProps = dispatch => ({ 32 | addItem: item => dispatch(addItem(item)) 33 | }); 34 | 35 | export default connect( 36 | null, 37 | mapDispatchToProps 38 | )(CollectionItem); 39 | -------------------------------------------------------------------------------- /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 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 | 9 | import './collections-overview.styles.scss'; 10 | 11 | const CollectionsOverview = ({ collections }) => ( 12 |
13 | {collections.map(({ id, ...otherCollectionProps }) => ( 14 | 15 | ))} 16 |
17 | ); 18 | 19 | const mapStateToProps = createStructuredSelector({ 20 | collections: selectCollectionsForPreview 21 | }); 22 | 23 | export default connect(mapStateToProps)(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 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 './directory.styles.scss'; 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.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 from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import { createStructuredSelector } from 'reselect'; 5 | 6 | import { auth } from '../../firebase/firebase.utils'; 7 | import CartIcon from '../cart-icon/cart-icon.component'; 8 | import CartDropdown from '../cart-dropdown/cart-dropdown.component'; 9 | import { selectCartHidden } from '../../redux/cart/cart.selectors'; 10 | import { selectCurrentUser } from '../../redux/user/user.selectors'; 11 | 12 | import { ReactComponent as Logo } from '../../assets/crown.svg'; 13 | 14 | import './header.styles.scss'; 15 | 16 | const Header = ({ currentUser, hidden }) => ( 17 |
18 | 19 | 20 | 21 |
22 | 23 | SHOP 24 | 25 | 26 | CONTACT 27 | 28 | {currentUser ? ( 29 |
auth.signOut()}> 30 | SIGN OUT 31 |
32 | ) : ( 33 | 34 | SIGN IN 35 | 36 | )} 37 | 38 |
39 | {hidden ? null : } 40 |
41 | ); 42 | 43 | const mapStateToProps = createStructuredSelector({ 44 | currentUser: selectCurrentUser, 45 | hidden: selectCartHidden 46 | }); 47 | 48 | export default connect(mapStateToProps)(Header); 49 | -------------------------------------------------------------------------------- /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/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 | 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 CheckoutItem from '../../components/checkout-item/checkout-item.component'; 6 | 7 | import { 8 | selectCartItems, 9 | selectCartTotal 10 | } from '../../redux/cart/cart.selectors'; 11 | 12 | import './checkout.styles.scss'; 13 | 14 | const CheckoutPage = ({ cartItems, total }) => ( 15 |
16 |
17 |
18 | Product 19 |
20 |
21 | Description 22 |
23 |
24 | Quantity 25 |
26 |
27 | Price 28 |
29 |
30 | Remove 31 |
32 |
33 | {cartItems.map(cartItem => ( 34 | 35 | ))} 36 |
TOTAL: ${total}
37 |
38 | ); 39 | 40 | const mapStateToProps = createStructuredSelector({ 41 | cartItems: selectCartItems, 42 | total: selectCartTotal 43 | }); 44 | 45 | export default connect(mapStateToProps)(CheckoutPage); 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 './collection.styles.scss'; 9 | 10 | const CollectionPage = ({ collection }) => { 11 | const { title, items } = collection; 12 | return ( 13 |
14 |

{title}

15 |
16 | {items.map(item => ( 17 | 18 | ))} 19 |
20 |
21 | ); 22 | }; 23 | 24 | const mapStateToProps = (state, ownProps) => ({ 25 | collection: selectCollection(ownProps.match.params.collectionId)(state) 26 | }); 27 | 28 | export default connect(mapStateToProps)(CollectionPage); 29 | -------------------------------------------------------------------------------- /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/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 = [logger]; 8 | 9 | export const store = createStore(rootReducer, applyMiddleware(...middlewares)); 10 | 11 | export const persistor = persistStore(store); 12 | 13 | export default { store, persistStore }; 14 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------