diff --git a/src/components/app/app.js b/src/components/app/app.js index 8c56569..865ee0d 100644 --- a/src/components/app/app.js +++ b/src/components/app/app.js @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Route, Switch, Redirect } from 'react-router-dom'; import RestaurantsPage from '../../pages/restaurants-page'; import Header from '../header'; import Basket from '../basket'; @@ -13,10 +13,10 @@ const App = () => {
- +

Error Page

} /> - '404 - Not found'} /> +
diff --git a/src/components/basket/basket.js b/src/components/basket/basket.js index f37d68b..85af38a 100644 --- a/src/components/basket/basket.js +++ b/src/components/basket/basket.js @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; +import { Link, withRouter } from 'react-router-dom'; import { createStructuredSelector } from 'reselect'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; @@ -10,11 +10,49 @@ import styles from './basket.module.css'; import itemStyles from './basket-item/basket-item.module.css'; import BasketItem from './basket-item'; import Button from '../button'; -import { orderProductsSelector, totalSelector } from '../../redux/selectors'; +import { + orderProductsSelector, + totalSelector, + locationSelector, + orderLoadingSelector, + errorOrderSelector, + successOrderSelector, +} from '../../redux/selectors'; import { UserConsumer } from '../../contexts/user-context'; +import { takeOrder } from '../../redux/actions'; +import LoadBanner from '../loadBanner'; -function Basket({ title = 'Basket', total, orderProducts }) { +function Basket({ + title = 'Basket', + total, + orderProducts, + loading, + location, + history, + takeOrder, + errorOrder, + successOrderMessage, +}) { // const { name } = useContext(userContext); + const canTakeOrder = useMemo(() => { + return location.pathname === '/checkout'; + }, [location]); + + function onBtnHandler() { + if (canTakeOrder) { + takeOrder(); + } else { + history.push('/checkout'); + } + } + + if (successOrderMessage) { + return ( +
+

{successOrderMessage}

+
+ ); + } if (!total) { return ( @@ -26,7 +64,7 @@ function Basket({ title = 'Basket', total, orderProducts }) { return (
- {/*

{`${name}'s ${title}`}

*/} + {loading && }

{({ name }) => `${name}'s ${title}`}

@@ -55,11 +93,10 @@ function Basket({ title = 'Basket', total, orderProducts }) {

{`${total} $`}

- - - +
{errorOrder}
+ ); } @@ -67,6 +104,10 @@ function Basket({ title = 'Basket', total, orderProducts }) { const mapStateToProps = createStructuredSelector({ total: totalSelector, orderProducts: orderProductsSelector, + location: locationSelector, + loading: orderLoadingSelector, + errorOrder: errorOrderSelector, + successOrderMessage: successOrderSelector, }); -export default connect(mapStateToProps)(Basket); +export default connect(mapStateToProps, { takeOrder })(withRouter(Basket)); diff --git a/src/components/loadBanner/index.js b/src/components/loadBanner/index.js new file mode 100644 index 0000000..114ac42 --- /dev/null +++ b/src/components/loadBanner/index.js @@ -0,0 +1 @@ +export { default } from './loadBanner'; diff --git a/src/components/loadBanner/loadBanner.js b/src/components/loadBanner/loadBanner.js new file mode 100644 index 0000000..c14a1e4 --- /dev/null +++ b/src/components/loadBanner/loadBanner.js @@ -0,0 +1,13 @@ +import React from 'react'; +import Loader from '../loader'; +import styles from './loadBanner.module.css'; + +function LoadBanner() { + return ( +
+ +
+ ); +} + +export default LoadBanner; diff --git a/src/components/loadBanner/loadBanner.module.css b/src/components/loadBanner/loadBanner.module.css new file mode 100644 index 0000000..a7a3120 --- /dev/null +++ b/src/components/loadBanner/loadBanner.module.css @@ -0,0 +1,10 @@ +.loadBanner { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + display: flex; + align-items: center; + background: rgb(0 0 0 / 18%); +} diff --git a/src/pages/restaurants-page.js b/src/pages/restaurants-page.js index 9e8e1ed..dfb748c 100644 --- a/src/pages/restaurants-page.js +++ b/src/pages/restaurants-page.js @@ -1,6 +1,6 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { connect } from 'react-redux'; -import { Route } from 'react-router-dom'; +import { Route, Redirect } from 'react-router-dom'; import { createStructuredSelector } from 'reselect'; import Restaurants from '../components/restaurants'; import Loader from '../components/loader'; @@ -12,20 +12,25 @@ import { } from '../redux/selectors'; import { loadRestaurants } from '../redux/actions'; -function RestaurantsPage({ loading, loaded, loadRestaurants, match }) { +function RestaurantsPage({ + restaurants, + loading, + loaded, + loadRestaurants, + match, +}) { useEffect(() => { if (!loading && !loaded) loadRestaurants(); }, [loading, loaded, loadRestaurants]); + const firstRestaurantId = useMemo(() => { + return restaurants.length ? restaurants[0].id : null; + }, [restaurants]); + if (loading || !loaded) return ; - if (match.isExact) { - return ( - <> - -

Select restaurant

- - ); + if (match.isExact && firstRestaurantId) { + return ; } return ; diff --git a/src/redux/actions.js b/src/redux/actions.js index 60ecd64..d5b1eed 100644 --- a/src/redux/actions.js +++ b/src/redux/actions.js @@ -7,12 +7,18 @@ import { LOAD_REVIEWS, LOAD_PRODUCTS, LOAD_USERS, + ORDER_SUCCESS, + ORDER_ERROR, + ORDER_LOADING_TOGGLE, + CLEAN_OUT, } from './constants'; +import products from './reducer/products'; import { usersLoadingSelector, usersLoadedSelector, reviewsLoadingSelector, reviewsLoadedSelector, + orderSelector, } from './selectors'; export const increment = (id) => ({ type: INCREMENT, payload: { id } }); @@ -63,3 +69,47 @@ export const loadUsers = () => async (dispatch, getState) => { dispatch(_loadUsers()); }; + +export const takeOrder = () => async (dispatch, getState) => { + const state = orderSelector(getState()); + const order = Object.keys(state).map((productId) => { + return { id: productId, amount: state[productId] }; + }); + + dispatch(orderLoading(true)); + + await fetch('/api/order', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(order), + }) + .then((res) => { + return res.json(); + }) + .then((res) => { + dispatch(orderLoading(false)); + if (res === 'ok') { + dispatch(orderSuccess()); + + return; + } + + dispatch(orderError(res)); + }) + .catch(dispatch(orderError)); +}; + +const orderSuccess = () => ({ + type: CLEAN_OUT, + payload: {}, +}); + +const orderLoading = (isLoad) => ({ + type: ORDER_LOADING_TOGGLE, + payload: { loading: isLoad }, +}); + +const orderError = (error) => ({ + type: ORDER_ERROR, + payload: { error }, +}); diff --git a/src/redux/constants.js b/src/redux/constants.js index 27ba575..1342ad4 100644 --- a/src/redux/constants.js +++ b/src/redux/constants.js @@ -11,3 +11,7 @@ export const LOAD_USERS = 'LOAD_USERS'; export const REQUEST = '_REQUEST'; export const SUCCESS = '_SUCCESS'; export const FAILURE = '_FAILURE'; + +export const ORDER_ERROR = 'ORDER_ERROR'; +export const ORDER_LOADING_TOGGLE = 'ORDER_LOADING_TOGGLE'; +export const CLEAN_OUT = 'CLEAN_OUT'; diff --git a/src/redux/reducer/order.js b/src/redux/reducer/order.js index 8837726..3d701e9 100644 --- a/src/redux/reducer/order.js +++ b/src/redux/reducer/order.js @@ -1,20 +1,65 @@ -import { DECREMENT, INCREMENT, REMOVE } from '../constants'; +import { + DECREMENT, + INCREMENT, + REMOVE, + ORDER_LOADING_TOGGLE, + ORDER_ERROR, + CLEAN_OUT, +} from '../constants'; // { [productId]: amount } -export default (state = {}, action) => { - const { type, payload } = action; + +const initialState = { + entities: {}, + loading: false, + error: null, + success: null, + // payload: 0, +}; + +export default (state = initialState, action) => { + const { type, payload, loading } = action; switch (type) { case INCREMENT: - return { ...state, [payload.id]: (state[payload.id] || 0) + 1 }; + return { + ...state, + entities: { + ...state.entities, + [payload.id]: (state.entities[payload.id] || 0) + 1, + }, + }; case DECREMENT: return { ...state, - [payload.id]: Math.max((state[payload.id] || 0) - 1, 0), + entities: { + ...state.entities, + [payload.id]: Math.max((state.entities[payload.id] || 0) - 1, 0), + }, }; case REMOVE: return { ...state, - [payload.id]: 0, + entities: { + ...state.entities, + [payload.id]: 0, + }, + }; + case ORDER_LOADING_TOGGLE: + return { + ...state, + loading: payload.loading, + }; + case ORDER_ERROR: + return { + ...state, + error: payload.error, + }; + case CLEAN_OUT: + return { + ...state, + entities: {}, + success: 'Заказ сформирован', + error: null, }; default: return state; diff --git a/src/redux/selectors.js b/src/redux/selectors.js index ca4f2a9..73533b0 100644 --- a/src/redux/selectors.js +++ b/src/redux/selectors.js @@ -2,10 +2,20 @@ import { createSelector } from 'reselect'; import { getById } from './utils'; const restaurantsSelector = (state) => state.restaurants.entities; -const orderSelector = (state) => state.order; +export const orderSelector = (state) => state.order.entities; const productsSelector = (state) => state.products.entities; const reviewsSelector = (state) => state.reviews.entities; const usersSelector = (state) => state.users.entities; +const historyRouterSelector = (state) => state.router; + +export const orderLoadingSelector = (state) => state.order.loading; +export const errorOrderSelector = (state) => state.order.error; +export const successOrderSelector = (state) => state.order.success; + +export const locationSelector = createSelector( + historyRouterSelector, + (history) => history.location +); export const restaurantsLoadingSelector = (state) => state.restaurants.loading; export const restaurantsLoadedSelector = (state) => state.restaurants.loaded;