├── public
├── favicon.ico
├── manifest.json
└── index.html
├── src
├── redux
│ ├── user
│ │ ├── user.types.js
│ │ ├── user.actions.js
│ │ ├── user.selectors.js
│ │ └── user.reducer.js
│ ├── cart
│ │ ├── cart.types.js
│ │ ├── cart.actions.js
│ │ ├── cart.selectors.js
│ │ ├── cart.utils.js
│ │ └── cart.reducer.js
│ ├── directory
│ │ ├── directory.selectors.js
│ │ └── directory.reducer.js
│ ├── shop
│ │ ├── shop.types.js
│ │ ├── shop.reducer.js
│ │ ├── shop.selectors.js
│ │ └── shop.actions.js
│ ├── store.js
│ └── root-reducer.js
├── pages
│ ├── homepage
│ │ ├── homepage.styles.jsx
│ │ └── homepage.component.jsx
│ ├── sign-in-and-sign-up
│ │ ├── sign-in-and-sign-up.styles.jsx
│ │ └── sign-in-and-sign-up.component.jsx
│ ├── collection
│ │ ├── collection.styles.jsx
│ │ ├── collection.container.jsx
│ │ └── collection.component.jsx
│ ├── checkout
│ │ ├── checkout.styles.jsx
│ │ └── checkout.component.jsx
│ └── shop
│ │ └── shop.component.jsx
├── App.css
├── components
│ ├── collections-overview
│ │ ├── collections-overview.styles.jsx
│ │ ├── collections-overview.container.jsx
│ │ └── collections-overview.component.jsx
│ ├── directory
│ │ ├── directory.styles.jsx
│ │ └── directory.component.jsx
│ ├── sign-up
│ │ ├── sign-up.styles.jsx
│ │ └── sign-up.component.jsx
│ ├── custom-button
│ │ ├── custom-button.component.jsx
│ │ └── custom-button.styles.jsx
│ ├── sign-in
│ │ ├── sign-in.styles.jsx
│ │ └── sign-in.component.jsx
│ ├── cart-item
│ │ ├── cart-item.styles.jsx
│ │ └── cart-item.component.jsx
│ ├── with-spinner
│ │ ├── with-spinner.component.jsx
│ │ └── with-spinner.styles.jsx
│ ├── collection-preview
│ │ ├── collection-preview.styles.jsx
│ │ └── collection-preview.component.jsx
│ ├── form-input
│ │ ├── form-input.component.jsx
│ │ └── form-input.styles.jsx
│ ├── cart-icon
│ │ ├── cart-icon.styles.jsx
│ │ └── cart-icon.component.jsx
│ ├── header
│ │ ├── header.styles.jsx
│ │ └── header.component.jsx
│ ├── cart-dropdown
│ │ ├── cart-dropdown.styles.jsx
│ │ └── cart-dropdown.component.jsx
│ ├── stripe-button
│ │ └── stripe-button.component.jsx
│ ├── menu-item
│ │ ├── menu-item.component.jsx
│ │ └── menu-item.styles.jsx
│ ├── checkout-item
│ │ ├── checkout-item.styles.jsx
│ │ └── checkout-item.component.jsx
│ └── collection-item
│ │ ├── collection-item.component.jsx
│ │ └── collection-styles.styles.jsx
├── App.test.js
├── index.css
├── index.js
├── assets
│ ├── crown.svg
│ └── shopping-bag.svg
├── firebase
│ └── firebase.utils.js
├── App.js
├── logo.svg
└── serviceWorker.js
├── .gitignore
├── package.json
└── README.md
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZhangMYihua/lesson-28/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/redux/user/user.types.js:
--------------------------------------------------------------------------------
1 | export const UserActionTypes = {
2 | SET_CURRENT_USER: 'SET_CURRENT_USER'
3 | };
4 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
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/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/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 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
26 | CRWN Clothing
27 |
28 |
29 | You need to enable JavaScript to run this app.
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/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/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 |
73 |
74 | );
75 | }
76 | }
77 |
78 | export default SignIn;
79 |
--------------------------------------------------------------------------------
/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/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/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/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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 |
98 |
99 | );
100 | }
101 | }
102 |
103 | export default SignUp;
104 |
--------------------------------------------------------------------------------
/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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------