diff --git a/src/components/app/app.js b/src/components/app/app.js index dde8600..d30c639 100644 --- a/src/components/app/app.js +++ b/src/components/app/app.js @@ -4,20 +4,33 @@ import Header from '../header'; import Basket from '../basket'; import { UserProvider } from '../../contexts/user-context'; import { useState } from 'react'; +import Error from '../error'; +import { + CurrencyProvider, + DEFAULT_CURRENCY, +} from '../../contexts/currency-context'; const App = () => { const [name, setName] = useState('Andrey'); + const [currency, setCurrency] = useState(DEFAULT_CURRENCY); + return (
-
- - - - -

Error Page!

} /> -

404 - Not found :(

} /> -
+ +
+ + + + + +

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..dfa0041 100644 --- a/src/components/basket/basket-item/basket-item.js +++ b/src/components/basket/basket-item/basket-item.js @@ -4,6 +4,7 @@ import { Link } from 'react-router-dom'; import { increment, decrement, remove } from '../../../redux/actions'; import Button from '../../button'; import styles from './basket-item.module.css'; +import { toCurrency } from '../../../contexts/currency-context'; function BasketItem({ product, @@ -25,7 +26,7 @@ function BasketItem({ {amount} - + ); } @@ -65,7 +71,12 @@ const mapStateToProps = (state) => { return { total: totalSelector(state), orderProducts: orderProductsSelector(state), + loading: orderLoadingSelector(state), }; }; -export default connect(mapStateToProps)(Basket); +const mapDispatchToProps = { + checkout, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Basket); diff --git a/src/components/error/error.js b/src/components/error/error.js new file mode 100644 index 0000000..696a5d8 --- /dev/null +++ b/src/components/error/error.js @@ -0,0 +1,36 @@ +import { useContext } from 'react'; +import { connect } from 'react-redux'; +import { + currencyContext, + DEFAULT_CURRENCY, + toCurrency, +} from '../../contexts/currency-context'; +import { orderErrorSelector, orderInfoSelector } from '../../redux/selectors'; + +function Error({ info, error }) { + const { currency } = useContext(currencyContext); + const currencyInfo = + info && currency !== DEFAULT_CURRENCY ? ( + $1 equals to {toCurrency(1)} + ) : ( + '' + ); + + return ( + <> +

Something went wrong!

+

{info}

+ {currencyInfo} +

{error}

+ + ); +} + +const mapStateToProps = (state) => { + return { + info: orderInfoSelector(state), + error: orderErrorSelector(state), + }; +}; + +export default connect(mapStateToProps)(Error); diff --git a/src/components/error/index.js b/src/components/error/index.js new file mode 100644 index 0000000..b706310 --- /dev/null +++ b/src/components/error/index.js @@ -0,0 +1 @@ +export { default } from './error'; diff --git a/src/components/header/header.js b/src/components/header/header.js index 3739d06..a4baf2a 100644 --- a/src/components/header/header.js +++ b/src/components/header/header.js @@ -3,15 +3,33 @@ import { Link } from 'react-router-dom'; import { userContext } from '../../contexts/user-context'; import { ReactComponent as Logo } from '../../icons/logo.svg'; import styles from './header.module.css'; +import { currencyContext, CURRENCIES } from '../../contexts/currency-context'; +import Button from '../button/button'; const Header = () => { const { name, setName } = useContext(userContext); + const { currency, setCurrency } = useContext(currencyContext); + + const currenciesList = Object.keys(CURRENCIES).map((item) => { + const selected = item === currency; + return ( + + ); + }); return (
setName('Igor')}> +
{currenciesList}

{name}

); diff --git a/src/components/product/product.js b/src/components/product/product.js index de900d6..893d2ac 100644 --- a/src/components/product/product.js +++ b/src/components/product/product.js @@ -4,6 +4,7 @@ import styles from './product.module.css'; import Button from '../button'; import { decrement, increment } from '../../redux/actions'; import { amountSelector, productSelector } from '../../redux/selectors'; +import { toCurrency } from '../../contexts/currency-context'; function Product({ product, amount, decrement, increment }) { return ( @@ -12,7 +13,7 @@ function Product({ product, amount, decrement, increment }) {

{product.name}

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

-
{product.price} $
+
{toCurrency(product.price)}
diff --git a/src/components/reviews/review/review.module.css b/src/components/reviews/review/review.module.css index 94455a8..7c20d46 100644 --- a/src/components/reviews/review/review.module.css +++ b/src/components/reviews/review/review.module.css @@ -55,3 +55,24 @@ line-height: 20px; } } + +.review-item-enter { + opacity: 0; + transform: scale(0.9); +} + +.review-item-enter-active { + opacity: 1; + transform: translateX(0); + transition: opacity 5000ms, transform 5000ms; +} + +.review-item-exit { + opacity: 1; +} + +.review-item-exit-active { + opacity: 0; + transform: scale(0.9); + transition: opacity 5000ms, transform 5000ms; +} diff --git a/src/components/reviews/reviews.js b/src/components/reviews/reviews.js index 2b09fda..d826542 100644 --- a/src/components/reviews/reviews.js +++ b/src/components/reviews/reviews.js @@ -5,12 +5,14 @@ import Review from './review'; import Loader from '../loader'; import ReviewForm from './review-form'; import styles from './reviews.module.css'; +import reviewStyles from './review/review.module.css'; import { loadReviews, loadUsers } from '../../redux/actions'; import { reviewsLoadedSelector, usersLoadedSelector, } from '../../redux/selectors'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; const Reviews = ({ reviews, @@ -27,11 +29,26 @@ const Reviews = ({ if (!usersLoaded || !reviewsLoaded) return ; + console.log('ff: ', reviewStyles); + return (
- {reviews.map((id) => ( - - ))} + + {reviews.map((id) => ( + + + + ))} +
); diff --git a/src/contexts/currency-context.js b/src/contexts/currency-context.js new file mode 100644 index 0000000..f9cc2d1 --- /dev/null +++ b/src/contexts/currency-context.js @@ -0,0 +1,20 @@ +import { createContext } from 'react'; + +export const DEFAULT_CURRENCY = 'USD'; +export const CURRENCIES = { + USD: 1, + RUB: 72, + BYN: 2.5, + UAH: 27, +}; + +export const currencyContext = createContext(DEFAULT_CURRENCY); + +export const CurrencyProvider = currencyContext.Provider; +export const CurrencyConsumer = currencyContext.Consumer; + +export const toCurrency = (value) => ( + + {({ currency }) => `${value * CURRENCIES[currency]} ${currency}`} + +); diff --git a/src/contexts/user-context.js b/src/contexts/user-context.js index 6932695..b67cda2 100644 --- a/src/contexts/user-context.js +++ b/src/contexts/user-context.js @@ -4,3 +4,7 @@ export const userContext = createContext('Default user'); export const UserProvider = userContext.Provider; export const UserConsumer = userContext.Consumer; + +export const printUser = () => ( + {({ name }) => name} +); diff --git a/src/redux/actions.js b/src/redux/actions.js index ef07010..9d39e09 100644 --- a/src/redux/actions.js +++ b/src/redux/actions.js @@ -1,4 +1,4 @@ -import { replace } from 'connected-react-router'; +import { replace, push } from 'connected-react-router'; import { DECREMENT, INCREMENT, @@ -12,6 +12,7 @@ import { REQUEST, SUCCESS, FAILURE, + SEND_ORDER, } from './constants'; import { @@ -19,6 +20,9 @@ import { usersLoadedSelector, reviewsLoadingSelector, reviewsLoadedSelector, + locationSelector, + orderSelector, + orderLoadedSelector, } from './selectors'; export const increment = (id) => ({ type: INCREMENT, id }); @@ -77,3 +81,39 @@ export const loadUsers = () => async (dispatch, getState) => { dispatch(_loadUsers()); }; + +export const checkout = () => async (dispatch, getState) => { + const state = getState(); + const location = locationSelector(state); + + if (location.pathname !== '/checkout') { + return dispatch(push('/checkout')); + } + + dispatch({ type: SEND_ORDER + REQUEST }); + + try { + const order = orderSelector(state); + const orderData = Object.entries(order).map(([id, amount]) => ({ + id, + amount: amount, + })); + + const result = await fetch('/api/order', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(orderData), + }); + + const info = await result.json(); + + dispatch({ type: SEND_ORDER + SUCCESS, info, status: result.status }); + + const loaded = orderLoadedSelector(getState()); + + dispatch(push(loaded ? '/success' : '/error')); + } catch (error) { + dispatch({ type: SEND_ORDER + FAILURE, error }); + dispatch(push('/error')); + } +}; diff --git a/src/redux/constants.js b/src/redux/constants.js index 6429975..efb66de 100644 --- a/src/redux/constants.js +++ b/src/redux/constants.js @@ -10,6 +10,8 @@ export const LOAD_PRODUCTS = 'LOAD_PRODUCTS'; export const LOAD_REVIEWS = 'LOAD_REVIEWS'; export const LOAD_USERS = 'LOAD_USERS'; +export const SEND_ORDER = 'SEND_ORDER'; + export const REQUEST = '_REQUEST'; export const SUCCESS = '_SUCCESS'; export const FAILURE = '_FAILURE'; diff --git a/src/redux/reducer/order.js b/src/redux/reducer/order.js index e20530c..3ae0e14 100644 --- a/src/redux/reducer/order.js +++ b/src/redux/reducer/order.js @@ -1,16 +1,56 @@ -import { DECREMENT, INCREMENT, REMOVE } from '../constants'; +import produce from 'immer'; +import { + DECREMENT, + INCREMENT, + REMOVE, + SEND_ORDER, + REQUEST, + SUCCESS, + FAILURE, +} from '../constants'; -// { [productId]: amount } -export default function (state = {}, action) { - const { type, id } = action; - switch (type) { - case INCREMENT: - return { ...state, [id]: (state[id] || 0) + 1 }; - case DECREMENT: - return { ...state, [id]: state[id] > 0 ? (state[id] || 0) - 1 : 0 }; - case REMOVE: - return { ...state, [id]: 0 }; - default: - return state; - } -} +const initialState = { + entities: {}, + loading: false, + loaded: false, + info: null, + error: null, +}; + +export default (state = initialState, action) => + produce(state, (draft) => { + const { type, id, info, error, status = 200 } = action; + const entities = draft.entities; + + switch (type) { + case INCREMENT: + entities[id] = (entities[id] || 0) + 1; + break; + case DECREMENT: + entities[id] = entities[id] > 0 ? (entities[id] || 0) - 1 : 0; + break; + case REMOVE: + entities[id] = 0; + break; + case SEND_ORDER + REQUEST: { + draft.loading = true; + draft.info = null; + draft.error = null; + break; + } + case SEND_ORDER + SUCCESS: { + draft.loading = false; + draft.loaded = status === 200; + draft.info = info; + break; + } + case SEND_ORDER + FAILURE: { + draft.loading = false; + draft.loaded = false; + draft.error = error; + break; + } + default: + return; + } + }); diff --git a/src/redux/selectors.js b/src/redux/selectors.js index 94b62a0..0083841 100644 --- a/src/redux/selectors.js +++ b/src/redux/selectors.js @@ -2,10 +2,11 @@ import { createSelector } from 'reselect'; const restaurantsSelector = (state) => state.restaurants.entities; const productsSelector = (state) => state.products.entities; -const orderSelector = (state) => state.order; const reviewsSelector = (state) => state.reviews.entities; const usersSelector = (state) => state.users.entities; +const routerSelector = (state) => state.router; +export const orderSelector = (state) => state.order.entities; export const activeIdRestaurantSelector = (state) => state.restaurants.activeId; export const restaurantsLoadingSelector = (state) => state.restaurants.loading; export const restaurantsLoadedSelector = (state) => state.restaurants.loaded; @@ -15,6 +16,11 @@ export const productsLoadingSelector = (state, props) => export const productsLoadedSelector = (state, props) => state.products.loaded[props.restId]; +export const orderLoadingSelector = (state) => state.order.loading; +export const orderLoadedSelector = (state) => state.order.loaded; +export const orderInfoSelector = (state) => state.order.info; +export const orderErrorSelector = (state) => state.order.error; + export const reviewsLoadingSelector = (state, props) => state.reviews.loading[props.restId]; export const reviewsLoadedSelector = (state, props) => @@ -88,3 +94,7 @@ export const averageRatingSelector = createSelector( ); } ); + +export const locationSelector = createSelector(routerSelector, (router) => { + return router.location; +});