diff --git a/src/components/app/app.js b/src/components/app/app.js index dde8600..f7ceaed 100644 --- a/src/components/app/app.js +++ b/src/components/app/app.js @@ -2,22 +2,32 @@ import { Redirect, Route, Switch } from 'react-router-dom'; import Restaurants from '../restaurants'; import Header from '../header'; import Basket from '../basket'; +import OrderError from '../order-result'; import { UserProvider } from '../../contexts/user-context'; import { useState } from 'react'; +import { CurrencyProvider } from '../../contexts/currency-context'; const App = () => { const [name, setName] = useState('Andrey'); + const [current, setCurrent] = useState('dollar'); return (
-
- - - - -

Error Page!

} /> -

404 - Not found :(

} /> -
+ +
+ + + + +

Error Page!

} /> + +

Thanks for order!

} + /> +

404 - Not found :(

} /> +
+
); diff --git a/src/components/basket/basket-item/basket-item.js b/src/components/basket/basket-item/basket-item.js index 0adbd14..0aa8b66 100644 --- a/src/components/basket/basket-item/basket-item.js +++ b/src/components/basket/basket-item/basket-item.js @@ -3,6 +3,7 @@ import cn from 'classnames'; import { Link } from 'react-router-dom'; import { increment, decrement, remove } from '../../../redux/actions'; import Button from '../../button'; +import Money from '../../money'; import styles from './basket-item.module.css'; function BasketItem({ @@ -25,7 +26,9 @@ function BasketItem({ {amount} - + ); } @@ -65,7 +77,11 @@ const mapStateToProps = (state) => { return { total: totalSelector(state), orderProducts: orderProductsSelector(state), + sending: chechoutSendingSelector(state), }; }; -export default connect(mapStateToProps)(Basket); +const mapDispatchToProps = { + checkoutOrder, +}; +export default connect(mapStateToProps, mapDispatchToProps)(Basket); diff --git a/src/components/basket/basket.module.css b/src/components/basket/basket.module.css index cdcf2e1..a2e9aee 100644 --- a/src/components/basket/basket.module.css +++ b/src/components/basket/basket.module.css @@ -24,3 +24,9 @@ padding: 20px 24px; } } + +.disable { + user-select: none; + pointer-events: none; + box-shadow: inset 0 0 0 500px rgba(0, 0, 0, 0.5); +} diff --git a/src/components/header/header.js b/src/components/header/header.js index 3739d06..bcf10fb 100644 --- a/src/components/header/header.js +++ b/src/components/header/header.js @@ -1,14 +1,31 @@ import { useContext } from 'react'; +import cn from 'classnames'; import { Link } from 'react-router-dom'; import { userContext } from '../../contexts/user-context'; import { ReactComponent as Logo } from '../../icons/logo.svg'; +import { + currDictionary, + currencyContext, +} from '../../contexts/currency-context'; import styles from './header.module.css'; const Header = () => { const { name, setName } = useContext(userContext); + const { current, setCurrent } = useContext(currencyContext); return (
setName('Igor')}> +
+ {Object.entries(currDictionary).map(([id, curr]) => ( + setCurrent(id)} + > + {curr.name} + + ))} +
diff --git a/src/components/header/header.module.css b/src/components/header/header.module.css index 8d48669..8c7e20a 100644 --- a/src/components/header/header.module.css +++ b/src/components/header/header.module.css @@ -13,3 +13,24 @@ right: 20px; top: 0; } + +.tabs { + height: auto; + text-align: center; + margin-right: auto; + padding: 12px; + background-color: var(--grey); +} + +.tabs span { + cursor: pointer; +} + +.tab { + padding: 4px 12px; + color: var(--black); +} + +.tab.active { + border-bottom: 1px solid var(--black); +} diff --git a/src/components/money/index.js b/src/components/money/index.js new file mode 100644 index 0000000..ad32a13 --- /dev/null +++ b/src/components/money/index.js @@ -0,0 +1 @@ +export { default } from './money'; diff --git a/src/components/money/money.js b/src/components/money/money.js new file mode 100644 index 0000000..abfc5f1 --- /dev/null +++ b/src/components/money/money.js @@ -0,0 +1,13 @@ +import { + CurrencyConsumer, + getMoneyInCurr, + currDictionary, +} from '../../contexts/currency-context'; + +export default function Money({ value }) { + return ( + + {({ current }) => getMoneyInCurr(value, current, currDictionary)} + + ); +} diff --git a/src/components/order-result/index.js b/src/components/order-result/index.js new file mode 100644 index 0000000..5444499 --- /dev/null +++ b/src/components/order-result/index.js @@ -0,0 +1 @@ +export { default } from './order-error'; diff --git a/src/components/order-result/order-error.js b/src/components/order-result/order-error.js new file mode 100644 index 0000000..d90f292 --- /dev/null +++ b/src/components/order-result/order-error.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { chechoutErrorSelector } from '../../redux/selectors'; + +const OrderError = ({ error = '' }) => { + return ( +
+

Wrong order!

+

{error}

+
+ ); +}; + +const mapStateToProps = (state) => ({ + error: chechoutErrorSelector(state), +}); + +export default connect(mapStateToProps)(OrderError); diff --git a/src/components/product/product.js b/src/components/product/product.js index de900d6..93ed27c 100644 --- a/src/components/product/product.js +++ b/src/components/product/product.js @@ -2,6 +2,7 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import styles from './product.module.css'; import Button from '../button'; +import Money from '../money'; import { decrement, increment } from '../../redux/actions'; import { amountSelector, productSelector } from '../../redux/selectors'; @@ -12,12 +13,14 @@ function Product({ product, amount, decrement, increment }) {

{product.name}

{product.ingredients.join(', ')}

-
{product.price} $
+
+ +
- {amount} +
); }; diff --git a/src/contexts/currency-context.js b/src/contexts/currency-context.js new file mode 100644 index 0000000..4156bb0 --- /dev/null +++ b/src/contexts/currency-context.js @@ -0,0 +1,27 @@ +import { createContext } from 'react'; + +export const currDictionary = { + ruble: { + name: 'руб.', + rateToDollar: 70, + }, + dollar: { + name: '$', + rateToDollar: 1, + }, + euro: { + name: 'eur.', + rateToDollar: 0.8, + }, +}; + +export const getMoneyInCurr = (value, curr, currDictionary) => { + const countedVal = value * currDictionary[curr].rateToDollar; + const roundedVal = parseInt(countedVal * 100) / 100; + return `${roundedVal} ${currDictionary[curr].name}`; +}; + +export const currencyContext = createContext(); + +export const CurrencyProvider = currencyContext.Provider; +export const CurrencyConsumer = currencyContext.Consumer; diff --git a/src/redux/actions.js b/src/redux/actions.js index ef07010..67c863b 100644 --- a/src/redux/actions.js +++ b/src/redux/actions.js @@ -1,4 +1,4 @@ -import { replace } from 'connected-react-router'; +import { push, replace } from 'connected-react-router'; import { DECREMENT, INCREMENT, @@ -12,6 +12,7 @@ import { REQUEST, SUCCESS, FAILURE, + CHECKOUT, } from './constants'; import { @@ -19,6 +20,8 @@ import { usersLoadedSelector, reviewsLoadingSelector, reviewsLoadedSelector, + orderToCheckoutSelector, + routePathSelector, } from './selectors'; export const increment = (id) => ({ type: INCREMENT, id }); @@ -77,3 +80,40 @@ export const loadUsers = () => async (dispatch, getState) => { dispatch(_loadUsers()); }; + +export const checkoutOrder = () => async (dispatch, getState) => { + const state = getState(); + const path = routePathSelector(state); + + switch (path) { + case '/checkout': + { + dispatch({ type: CHECKOUT + REQUEST }); + const order = orderToCheckoutSelector(state); + try { + const response = await fetch('/api/order', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(order), + }); + + const status = response.status; + if (status === 200) { + dispatch({ type: CHECKOUT + SUCCESS }); + dispatch(push('/orser-success')); + } else { + const error = await response.json(); + dispatch({ type: CHECKOUT + FAILURE, error }); + dispatch(push('/orser-failed', error)); + } + } catch (error) { + dispatch({ type: CHECKOUT + FAILURE, error }); + dispatch(push('/error')); + } + } + break; + default: + dispatch(push('/checkout')); + break; + } +}; diff --git a/src/redux/constants.js b/src/redux/constants.js index 6429975..8354542 100644 --- a/src/redux/constants.js +++ b/src/redux/constants.js @@ -1,6 +1,7 @@ export const INCREMENT = 'INCREMENT'; export const DECREMENT = 'DECREMENT'; export const REMOVE = 'REMOVE'; + export const ADD_REVIEW = 'ADD_REVIEW'; export const CHANGE_RESTAURANT = 'CHANGE_RESTAURANT'; @@ -9,6 +10,7 @@ export const LOAD_RESTAURANTS = 'LOAD_RESTAURANTS'; export const LOAD_PRODUCTS = 'LOAD_PRODUCTS'; export const LOAD_REVIEWS = 'LOAD_REVIEWS'; export const LOAD_USERS = 'LOAD_USERS'; +export const CHECKOUT = 'CHECKOUT'; export const REQUEST = '_REQUEST'; export const SUCCESS = '_SUCCESS'; diff --git a/src/redux/reducer/order.js b/src/redux/reducer/order.js index e20530c..00a4e2f 100644 --- a/src/redux/reducer/order.js +++ b/src/redux/reducer/order.js @@ -1,16 +1,51 @@ -import { DECREMENT, INCREMENT, REMOVE } from '../constants'; +import produce from 'immer'; +import { + DECREMENT, + INCREMENT, + REMOVE, + CHECKOUT, + REQUEST, + SUCCESS, + FAILURE, +} from '../constants'; + +const initialState = { + sending: false, + sended: false, + entities: {}, + error: null, +}; // { [productId]: amount } -export default function (state = {}, action) { - const { type, id } = action; +export default produce((draft = initialState, action) => { + const { type, id, error } = action; switch (type) { case INCREMENT: - return { ...state, [id]: (state[id] || 0) + 1 }; + draft.entities[id] = (draft.entities[id] || 0) + 1; + break; case DECREMENT: - return { ...state, [id]: state[id] > 0 ? (state[id] || 0) - 1 : 0 }; + draft.entities[id] = + draft.entities[id] > 0 ? (draft.entities[id] || 0) - 1 : 0; + break; case REMOVE: - return { ...state, [id]: 0 }; + draft.entities[id] = 0; + break; + case CHECKOUT + REQUEST: { + draft.error = null; + draft.sending = true; + break; + } + case CHECKOUT + SUCCESS: { + draft.sending = false; + draft.entities = {}; + break; + } + case CHECKOUT + FAILURE: { + draft.sending = false; + draft.error = error; + break; + } default: - return state; + return draft; } -} +}); diff --git a/src/redux/selectors.js b/src/redux/selectors.js index 94b62a0..9e044ca 100644 --- a/src/redux/selectors.js +++ b/src/redux/selectors.js @@ -2,9 +2,10 @@ import { createSelector } from 'reselect'; const restaurantsSelector = (state) => state.restaurants.entities; const productsSelector = (state) => state.products.entities; -const orderSelector = (state) => state.order; +const orderSelector = (state) => state.order.entities; const reviewsSelector = (state) => state.reviews.entities; const usersSelector = (state) => state.users.entities; +const routeLocationSelector = (state) => state.router.location; export const activeIdRestaurantSelector = (state) => state.restaurants.activeId; export const restaurantsLoadingSelector = (state) => state.restaurants.loading; @@ -88,3 +89,16 @@ export const averageRatingSelector = createSelector( ); } ); + +export const orderToCheckoutSelector = createSelector(orderSelector, (order) => + Object.entries(order).reduce( + (acc, [id, amount]) => [...acc, { id, amount }], + [] + ) +); + +export const routePathSelector = (state) => + routeLocationSelector(state).pathname; + +export const chechoutSendingSelector = (state) => state.order.sending; +export const chechoutErrorSelector = (state) => state.order.error;