diff --git a/README.md b/README.md index dad2ff4..e51503d 100644 --- a/README.md +++ b/README.md @@ -47,3 +47,26 @@ 5. При загрузках показывать лоадеры, все грузить максимально низко, там где эти данные нужны 6. Все данные грузить только один раз (не загружать повторно данные, которые уже есть) 7. (Опционально) переписать все на **immer** + +# HT6 + +<<<<<<< HEAD + +1. Сделать reviews/menu отдельными роутами (/restaurant/:id/reviews) +2. # В корзине сделать продукты линками на их ресторан +3. Сделать reviews/menu отдельными роутами (/restaurants/:id/reviews) +4. В корзине сделать продукты линками на их ресторан + +## HT7 + +1. Сделать редирект со **/** и с **/restaurants** на страницу ресторана +2. Проверить если мы на **/checkout**, то при нажатии на кнопку: + +- отправить **POST** запрос на: '/api/order' с **JSON** формата [{id: "d75f762a-eadd-49be-8918-ed0daa8dd024", amount: 2}] +- блокировать кнопку на время запроса (можно добавить лоадер) +- при успешном ответе (при сумме заказа от 50 до 200) редиректить на новую страницу "Спасибо за заказ!" и очищать корзину +- при ошибке редиректить на странцу ошибки, показать текст ошибки + +3. Реализовать переключение валюты, хранить словарь словарь в контексте (минимум 3 валюты) +4. Анимировать добавление ревью использус css modules + > > > > > > > 166edbb61a24a152c0ff97469b26dfa82afad873 diff --git a/package.json b/package.json index 077e060..193b62f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@wojtekmaj/enzyme-adapter-react-17": "^0.4.1", "classnames": "^2.2.6", "concurrently": "^5.3.0", + "connected-react-router": "^6.8.0", "enzyme": "^3.11.0", "immer": "^8.0.0", "normalize.css": "^8.0.1", @@ -19,6 +20,7 @@ "react-redux": "^7.2.2", "react-router-dom": "^5.2.0", "react-scripts": "4.0.1", + "react-transition-group": "^4.4.1", "redux": "^4.0.5", "redux-devtools-extension": "^2.13.8", "redux-thunk": "^2.3.0", diff --git a/simple_api/api/index.js b/simple_api/api/index.js index c2d3313..ffb2980 100644 --- a/simple_api/api/index.js +++ b/simple_api/api/index.js @@ -34,4 +34,21 @@ router.get('/users', (req, res, next) => { reply(res, users); }); +const min = (m) => `you ordered for $${m}, but the min order amount is $50`; +const max = (m) => `you ordered for $${m}, but the max order amount is $200`; + +router.post('/order', function (req, res, next) { + try { + const total = req.body + .map((it) => products.find((p) => p.id === it.id).price * it.amount) + .reduce((acc, next) => acc + next, 0); + + if (total < 50) return reply(res, min(total), 3000, 400); + if (total > 200) return reply(res, max(total), 3000, 400); + return reply(res, 'ok', 3000); + } catch { + return reply(res, 'wrong data', 1000, 400); + } +}); + module.exports = router; diff --git a/src/components/app/app.js b/src/components/app/app.js index 83215d1..f3faa75 100644 --- a/src/components/app/app.js +++ b/src/components/app/app.js @@ -1,21 +1,35 @@ -import React, { PureComponent } from 'react'; -import { Route, Switch } from 'react-router-dom'; +import React, { useState } from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; import RestaurantsPage from '../../pages/restaurants-page'; import Header from '../header'; import Basket from '../basket'; +import { UserProvider } from '../../contexts/user-context'; +import CheckOutSuccess from '../basket/checkOutResult/checkoutSuccess'; +import CheckOutFail from '../basket/checkOutResult/checkOutFail'; -export default class App extends PureComponent { - render() { - return ( -
+const App = () => { + const [name, setName] = useState('Igor'); + + return ( +
+
- 'Main page'} /> + + - '404 - Not found'} /> +

Error Page

} /> + { + return ; + }} + />
-
- ); - } -} + +
+ ); +}; + +export default App; diff --git a/src/components/basket/basket-item/basket-item.js b/src/components/basket/basket-item/basket-item.js index 534c734..7c2bdf5 100644 --- a/src/components/basket/basket-item/basket-item.js +++ b/src/components/basket/basket-item/basket-item.js @@ -1,22 +1,30 @@ import React from 'react'; -import { connect } from 'react-redux'; +import { connect, useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; import cn from 'classnames'; import { increment, decrement, remove } from '../../../redux/actions'; import Button from '../../button'; import styles from './basket-item.module.css'; +import { activeCurrencySelector } from '../../../redux/selectors'; function BasketItem({ product, amount, subtotal, + restaurantId, increment, decrement, remove, }) { + const activeCurrency = useSelector(activeCurrencySelector); + const [currencyName, currencyValue] = Object.entries(activeCurrency)[0]; + const localSubTotal = Math.round(subtotal * currencyValue); return (
- {product.name} + + {product.name} +
@@ -24,7 +32,9 @@ function BasketItem({ {amount}
-

{subtotal} $

+

+ {localSubTotal} {currencyName} +

diff --git a/src/components/basket/basket.css b/src/components/basket/basket.css new file mode 100644 index 0000000..603f016 --- /dev/null +++ b/src/components/basket/basket.css @@ -0,0 +1,20 @@ +.basket-animation-enter { + opacity: 0; + transform: scale(0.9); +} + +.basket-animation-enter-active { + opacity: 1; + transform: translateX(0); + transition: opacity 300ms, transform 300ms; +} + +.basket-animation-exit { + opacity: 1; +} + +.basket-animation-exit-active { + opacity: 0; + transform: scale(0.9); + transition: opacity 300ms, transform 300ms; +} diff --git a/src/components/basket/basket.js b/src/components/basket/basket.js index 8ae1b08..0e87240 100644 --- a/src/components/basket/basket.js +++ b/src/components/basket/basket.js @@ -1,15 +1,46 @@ import React from 'react'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; +import { connect, useDispatch, useSelector } from 'react-redux'; +import { Link, useHistory } from 'react-router-dom'; import { createStructuredSelector } from 'reselect'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import './basket.css'; import styles from './basket.module.css'; +import Loader from '../loader'; 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 { + activeCurrencySelector, + orderProductsSelector, + totalSelector, +} from '../../redux/selectors'; +import { UserConsumer } from '../../contexts/user-context'; +import { checkoutProducts } from '../../redux/actions'; function Basket({ title = 'Basket', total, orderProducts }) { + const activeCurrency = useSelector(activeCurrencySelector); + const [currencyName, currencyValue] = Object.entries(activeCurrency)[0]; + const history = useHistory(); + const dispatch = useDispatch(); + const checkOutState = useSelector((state) => state.checkout); + + const onCheckoutClickHandler = () => { + if (history.location.pathname === '/checkout') { + dispatch(checkoutProducts(orderProducts)); + } + }; + if (checkOutState.loading) { + return ; + } + if (checkOutState.loaded) { + history.push('/checkout/success'); + //return + } + if (checkOutState.error) { + history.replace('/checkout/error'); + // return + } if (!total) { return (
@@ -17,29 +48,40 @@ function Basket({ title = 'Basket', total, orderProducts }) {
); } - + const totalprice = Math.round(total * currencyValue); return (
-

{title}

- {orderProducts.map(({ product, amount, subtotal }) => ( - - ))} + {/*

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

*/} +

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

+ + {orderProducts.map(({ product, amount, subtotal, restaurantId }) => ( + + + + ))} +

Total

-

{`${total} $`}

+

{`${totalprice} ${currencyName}`}

- diff --git a/src/components/header/header.js b/src/components/header/header.js index 4997de9..e35f147 100644 --- a/src/components/header/header.js +++ b/src/components/header/header.js @@ -1,12 +1,51 @@ -import React from 'react'; +import React, { useContext } from 'react'; +import { + activeCurrencySelector, + allCurrenciesArraySelector, +} from '../../redux/selectors'; import Logo from './logo'; import styles from './header.module.css'; +import { userContext } from '../../contexts/user-context'; +import { useDispatch, useSelector } from 'react-redux'; +import { setCurrency } from '../../redux/actions'; -const Header = () => ( -
- -
-); +const Header = () => { + const allCurrenciesEnteries = useSelector(allCurrenciesArraySelector); + const activeCurrency = useSelector(activeCurrencySelector); + const dispatch = useDispatch(); + + const { name, setName } = useContext(userContext); + + return ( +
setName('Ivan')}> + + {/*

{name}

*/} +
+ {allCurrenciesEnteries.map((currArr) => { + const [name, value] = currArr; + return ( +

dispatch(setCurrency(name))} + > + {' '} + {name} : {value}{' '} +

+ ); + })} + {Object.entries(activeCurrency).map((activeItemArr) => { + const [name, value] = activeItemArr; + return ( +

+ {name} : {value} +

+ ); + })} +
+
+ ); +}; export default Header; diff --git a/src/components/header/header.module.css b/src/components/header/header.module.css index fe1e93a..0628b57 100644 --- a/src/components/header/header.module.css +++ b/src/components/header/header.module.css @@ -4,4 +4,40 @@ justify-content: center; height: 60px; background: var(--black); + position: relative; +} + +.header h2 { + position: absolute; + color: var(--white); + right: 20px; + top: 0; +} +.currencySwitcher { + display: flex; + align-items: center; + justify-content: space-around; + position: absolute; + color: var(--white); + right: 20px; + top: 0; +} + +.currencySwitcher :hover { + color: gold; +} + +.currencySwitcherActive { + color: gold; +} +.currencySwitcher h3 { + display: inline; + margin-left: 10px; +} +.currencyItem { + margin-right: 20px; +} +.currencyItemActive { + color: gold; + font-size: larger; } diff --git a/src/components/product/product.js b/src/components/product/product.js index a388df5..c3ce8bb 100644 --- a/src/components/product/product.js +++ b/src/components/product/product.js @@ -7,10 +7,17 @@ import styles from './product.module.css'; import { decrement, increment } from '../../redux/actions'; import Button from '../button'; -import { productAmountSelector, productSelector } from '../../redux/selectors'; +import { + productAmountSelector, + productSelector, + activeCurrencySelector, +} from '../../redux/selectors'; -const Product = ({ product, amount, increment, decrement }) => { +const Product = ({ product, amount, increment, decrement, currency }) => { if (!product) return null; + const activeCurrency = currency; + const [currencyName, currencyValue] = Object.entries(activeCurrency)[0]; + const price = Math.round(currencyValue * product.price); return (
@@ -18,7 +25,9 @@ const Product = ({ product, amount, increment, decrement }) => {

{product.name}

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

-
{product.price} $
+
+ {price} {currencyName} +
@@ -51,6 +60,7 @@ Product.propTypes = { const mapStateToProps = createStructuredSelector({ amount: productAmountSelector, product: productSelector, + currency: activeCurrencySelector, }); const mapDispatchToProps = (dispatch, ownProps) => ({ diff --git a/src/components/restaurant/restaurant.js b/src/components/restaurant/restaurant.js index c3bef92..b1b1f7d 100644 --- a/src/components/restaurant/restaurant.js +++ b/src/components/restaurant/restaurant.js @@ -1,5 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; +import { Route, Redirect, Switch } from 'react-router-dom'; import PropTypes from 'prop-types'; import { createStructuredSelector } from 'reselect'; @@ -13,11 +14,8 @@ import { averageRatingSelector } from '../../redux/selectors'; const Restaurant = ({ restaurant, averageRating }) => { const { id, name, menu, reviews } = restaurant; const tabs = [ - { title: 'Menu', content: }, - { - title: 'Reviews', - content: , - }, + { title: 'Menu', to: `/restaurants/${id}/menu` }, + { title: 'Reviews', to: `/restaurants/${id}/reviews` }, ]; return ( @@ -26,6 +24,21 @@ const Restaurant = ({ restaurant, averageRating }) => { {!!averageRating && } + + } + /> + } + /> + +
); }; diff --git a/src/components/restaurants/restaurants.js b/src/components/restaurants/restaurants.js index 5054773..ef8224b 100644 --- a/src/components/restaurants/restaurants.js +++ b/src/components/restaurants/restaurants.js @@ -1,32 +1,24 @@ import React from 'react'; import { connect } from 'react-redux'; -import { NavLink } from 'react-router-dom'; import { createStructuredSelector } from 'reselect'; import PropTypes from 'prop-types'; +import Tabs from '../tabs'; import Restaurant from '../restaurant'; import { restaurantsListSelector } from '../../redux/selectors'; -import styles from './restaurants.module.css'; - const Restaurants = ({ restaurants, match }) => { const { restId } = match.params; const restaurant = restaurants.find((restaurant) => restaurant.id === restId); + const tabs = restaurants.map(({ id, name }) => ({ + title: name, + to: `/restaurants/${id}`, + })); + return ( <> -
- {restaurants.map(({ id, name }, index) => ( - - {name} - - ))} -
- + + {restaurant && } ); }; diff --git a/src/components/restaurants/restaurants.module.css b/src/components/restaurants/restaurants.module.css deleted file mode 100644 index 654ebcd..0000000 --- a/src/components/restaurants/restaurants.module.css +++ /dev/null @@ -1,20 +0,0 @@ -.tabs { - height: auto; - text-align: center; - padding: 12px; - background-color: var(--grey); -} - -.tabs span { - cursor: pointer; -} - -.tab { - padding: 4px 12px; - color: var(--black); - text-decoration: none; -} - -.tab.active { - border-bottom: 1px solid var(--black); -} diff --git a/src/components/reviews/reviews.js b/src/components/reviews/reviews.js index e68fb1f..e8b7209 100644 --- a/src/components/reviews/reviews.js +++ b/src/components/reviews/reviews.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import Review from './review'; import ReviewForm from './review-form'; import styles from './reviews.module.css'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { loadReviews, loadUsers } from '../../redux/actions'; import { @@ -31,9 +32,14 @@ const Reviews = ({ return (
- {reviews.map((id) => ( - - ))} + + {reviews.map((id) => ( + + + + ))} + +
); diff --git a/src/components/reviews/reviews.module.css b/src/components/reviews/reviews.module.css index d8cc252..92f2ebe 100644 --- a/src/components/reviews/reviews.module.css +++ b/src/components/reviews/reviews.module.css @@ -4,3 +4,24 @@ max-width: 884px; width: 100%; } + +.review-animation-enter { + opacity: 0; + transform: scale(0.9); +} + +.review-animation-enter-active { + opacity: 1; + transform: translateX(0); + transition: opacity 1300ms, transform 1300ms; +} + +.review-animation-exit { + opacity: 1; +} + +.review-animation-exit-active { + opacity: 0; + transform: scale(0.9); + transition: opacity 1300ms, transform 1300ms; +} diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index b96b039..fc7e762 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -1,29 +1,23 @@ -import React, { useState } from 'react'; +import React from 'react'; +import { NavLink } from 'react-router-dom'; import PropTypes from 'prop-types'; -import cn from 'classnames'; import styles from './tabs.module.css'; const Tabs = ({ tabs }) => { - const [activeTab, setActiveTab] = useState(0); - - const { content } = tabs[activeTab]; - return ( - <> -
- {tabs.map(({ title }, index) => ( - setActiveTab(index)} - > - {title} - - ))} -
- {content} - +
+ {tabs.map(({ title, to }) => ( + + {title} + + ))} +
); }; @@ -31,7 +25,7 @@ Tabs.propTypes = { tabs: PropTypes.arrayOf( PropTypes.shape({ title: PropTypes.string.isRequired, - content: PropTypes.element.isRequired, + to: PropTypes.string.isRequired, }).isRequired ).isRequired, }; diff --git a/src/components/tabs/tabs.module.css b/src/components/tabs/tabs.module.css index ecf138e..654ebcd 100644 --- a/src/components/tabs/tabs.module.css +++ b/src/components/tabs/tabs.module.css @@ -11,6 +11,8 @@ .tab { padding: 4px 12px; + color: var(--black); + text-decoration: none; } .tab.active { diff --git a/src/contexts/user-context.js b/src/contexts/user-context.js new file mode 100644 index 0000000..c8a3456 --- /dev/null +++ b/src/contexts/user-context.js @@ -0,0 +1,6 @@ +import { createContext } from 'react'; + +export const userContext = createContext({ name: 'Default User' }); + +export const UserProvider = userContext.Provider; +export const UserConsumer = userContext.Consumer; diff --git a/src/history.js b/src/history.js new file mode 100644 index 0000000..564e8ec --- /dev/null +++ b/src/history.js @@ -0,0 +1,5 @@ +import { createBrowserHistory } from 'history'; + +const history = createBrowserHistory(); + +export default history; diff --git a/src/index.js b/src/index.js index 0a5051b..40881c1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,21 +1,22 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { BrowserRouter } from 'react-router-dom'; +import { ConnectedRouter } from 'connected-react-router'; import { Provider } from 'react-redux'; import './index.css'; import App from './components/app'; import store from './redux/store'; +import history from './history'; // DEV ONLY!!! window.store = store; ReactDOM.render( - - + + - - , + + , document.getElementById('root') ); diff --git a/src/pages/restaurants-page.js b/src/pages/restaurants-page.js index 8dc49b2..7086e90 100644 --- a/src/pages/restaurants-page.js +++ b/src/pages/restaurants-page.js @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { connect } from 'react-redux'; -import { Route, Link } from 'react-router-dom'; +import { Redirect, Route } from 'react-router-dom'; import { createStructuredSelector } from 'reselect'; import Restaurants from '../components/restaurants'; import Loader from '../components/loader'; @@ -13,11 +13,11 @@ import { import { loadRestaurants } from '../redux/actions'; function RestaurantsPage({ - restaurants, loading, loaded, loadRestaurants, match, + restaurants, }) { useEffect(() => { if (!loading && !loaded) loadRestaurants(); @@ -26,15 +26,12 @@ function RestaurantsPage({ if (loading || !loaded) return ; if (match.isExact) { + const [{ id: currentRestaurantId }] = restaurants; + return ( -
-
select page:
- {restaurants.map(({ id, name }) => ( -

- {name} -

- ))} -
+ <> + + ); } diff --git a/src/redux/actions.js b/src/redux/actions.js index 60ecd64..717358e 100644 --- a/src/redux/actions.js +++ b/src/redux/actions.js @@ -7,6 +7,12 @@ import { LOAD_REVIEWS, LOAD_PRODUCTS, LOAD_USERS, + CHECKOUT_PRODUCTS, + REQUEST, + SUCCESS, + FAILURE, + CLEAR_BASKET, + SET_CURRENCY, } from './constants'; import { usersLoadingSelector, @@ -14,10 +20,15 @@ import { reviewsLoadingSelector, reviewsLoadedSelector, } from './selectors'; - export const increment = (id) => ({ type: INCREMENT, payload: { id } }); export const decrement = (id) => ({ type: DECREMENT, payload: { id } }); export const remove = (id) => ({ type: REMOVE, payload: { id } }); +export const clearBasket = () => ({ type: CLEAR_BASKET }); + +export const setCurrency = (currencyName) => ({ + type: SET_CURRENCY, + payload: currencyName, +}); export const addReview = (review, restaurantId) => ({ type: ADD_REVIEW, @@ -63,3 +74,25 @@ export const loadUsers = () => async (dispatch, getState) => { dispatch(_loadUsers()); }; + +export const checkoutProducts = (basketItems) => async (dispatch) => { + const itemsArr = basketItems.map((basketItem) => { + return { id: basketItem.product.id, amount: basketItem.amount }; + }); + + dispatch({ type: CHECKOUT_PRODUCTS + REQUEST }); + + const request = await fetch('/api/order', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify([...itemsArr]), + }); + const response = await request.json(); + if (response !== 'ok') { + dispatch({ type: CHECKOUT_PRODUCTS + FAILURE, payload: response }); + } else { + dispatch({ type: CHECKOUT_PRODUCTS + SUCCESS, payload: response }); + } +}; diff --git a/src/redux/constants.js b/src/redux/constants.js index 27ba575..62437dc 100644 --- a/src/redux/constants.js +++ b/src/redux/constants.js @@ -1,13 +1,19 @@ export const INCREMENT = 'INCREMENT'; export const DECREMENT = 'DECREMENT'; export const REMOVE = 'REMOVE'; +export const CLEAR_BASKET = 'CLEAR_BASKET'; + export const ADD_REVIEW = 'ADD_REVIEW'; +export const SET_CURRENCY = 'SET_CURRENCY'; + 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_PRODUCTS = 'CHECKOUT_PRODUCTS'; + export const REQUEST = '_REQUEST'; export const SUCCESS = '_SUCCESS'; export const FAILURE = '_FAILURE'; diff --git a/src/redux/middleware/api.js b/src/redux/middleware/api.js index 54fd69f..f0e7f14 100644 --- a/src/redux/middleware/api.js +++ b/src/redux/middleware/api.js @@ -1,3 +1,4 @@ +import { replace } from 'connected-react-router'; import { REQUEST, SUCCESS, FAILURE } from '../constants'; export default (store) => (next) => async (action) => { @@ -12,5 +13,6 @@ export default (store) => (next) => async (action) => { next({ ...rest, type: type + SUCCESS, data }); } catch (error) { next({ ...rest, type: type + FAILURE, error }); + next(replace('/error')); } }; diff --git a/src/redux/reducer/index.js b/src/redux/reducer/index.js index 37463c9..92d26c8 100644 --- a/src/redux/reducer/index.js +++ b/src/redux/reducer/index.js @@ -1,15 +1,23 @@ import { combineReducers } from 'redux'; +import { connectRouter } from 'connected-react-router'; import order from './order'; import restaurants from './restaurants'; import products from './products'; import reviews from './reviews'; import users from './users'; +import checkout from './checkout'; +import currency from './currency'; + +import history from '../../history'; export default combineReducers({ + router: connectRouter(history), order, restaurants, products, reviews, users, + checkout, + currency, }); diff --git a/src/redux/reducer/order.js b/src/redux/reducer/order.js index 8837726..8b66eb0 100644 --- a/src/redux/reducer/order.js +++ b/src/redux/reducer/order.js @@ -1,4 +1,4 @@ -import { DECREMENT, INCREMENT, REMOVE } from '../constants'; +import { CLEAR_BASKET, DECREMENT, INCREMENT, REMOVE } from '../constants'; // { [productId]: amount } export default (state = {}, action) => { @@ -16,6 +16,8 @@ export default (state = {}, action) => { ...state, [payload.id]: 0, }; + case CLEAR_BASKET: + return {}; default: return state; } diff --git a/src/redux/selectors.js b/src/redux/selectors.js index c1af5a0..fef2317 100644 --- a/src/redux/selectors.js +++ b/src/redux/selectors.js @@ -6,7 +6,13 @@ const orderSelector = (state) => state.order; const productsSelector = (state) => state.products.entities; const reviewsSelector = (state) => state.reviews.entities; const usersSelector = (state) => state.users.entities; +const currenciesSelector = (state) => state.currency.entities; +export const activeCurrencySelector = (state) => state.currency.activeCurrency; +export const allCurrenciesArraySelector = createSelector( + currenciesSelector, + (currencies) => Object.entries(currencies) +); export const restaurantsLoadingSelector = (state) => state.restaurants.loading; export const restaurantsLoadedSelector = (state) => state.restaurants.loaded; @@ -32,10 +38,24 @@ export const productAmountSelector = getById(orderSelector, 0); export const productSelector = getById(productsSelector); const reviewSelector = getById(reviewsSelector); +const restaurantsIdsByProductsSelector = createSelector( + restaurantsListSelector, + (restaurants) => + restaurants + .flatMap((rest) => + rest.menu.map((productId) => ({ productId, restId: rest.id })) + ) + .reduce( + (acc, { productId, restId }) => ({ ...acc, [productId]: restId }), + {} + ) +); + export const orderProductsSelector = createSelector( productsSelector, orderSelector, - (products, order) => + restaurantsIdsByProductsSelector, + (products, order, restaurantsIds) => Object.keys(order) .filter((productId) => order[productId] > 0) .map((productId) => products[productId]) @@ -43,6 +63,7 @@ export const orderProductsSelector = createSelector( product, amount: order[product.id], subtotal: order[product.id] * product.price, + restaurantId: restaurantsIds[product.id], })) ); diff --git a/src/redux/store.js b/src/redux/store.js index 2915183..28b204a 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -1,12 +1,21 @@ import { applyMiddleware, createStore } from 'redux'; import thunk from 'redux-thunk'; import { composeWithDevTools } from 'redux-devtools-extension'; +import { routerMiddleware } from 'connected-react-router'; import reducer from './reducer'; import logger from './middleware/logger'; import generateId from './middleware/generateId'; import api from './middleware/api'; -const enhancer = applyMiddleware(thunk, api, generateId, logger); +import history from '../history'; + +const enhancer = applyMiddleware( + thunk, + api, + generateId, + routerMiddleware(history), + logger +); export default createStore(reducer, composeWithDevTools(enhancer)); diff --git a/yarn.lock b/yarn.lock index 69a6216..1b4ad46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1089,7 +1089,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== @@ -3478,6 +3478,13 @@ connect-history-api-fallback@^1.6.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== +connected-react-router@^6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/connected-react-router/-/connected-react-router-6.8.0.tgz#ddc687b31d498322445d235d660798489fa56cae" + integrity sha512-E64/6krdJM3Ag3MMmh2nKPtMbH15s3JQDuaYJvOVXzu6MbHbDyIvuwLOyhQIuP4Om9zqEfZYiVyflROibSsONg== + dependencies: + prop-types "^15.7.2" + console-browserify@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" @@ -3928,6 +3935,11 @@ cssstyle@^2.2.0: dependencies: cssom "~0.3.6" +csstype@^3.0.2: + version "3.0.6" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.6.tgz#865d0b5833d7d8d40f4e5b8a6d76aea3de4725ef" + integrity sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw== + cyclist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" @@ -4201,6 +4213,14 @@ dom-converter@^0.2: dependencies: utila "~0.4" +dom-helpers@^5.0.1: + version "5.2.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b" + integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -9523,6 +9543,16 @@ react-test-renderer@^17.0.0: react-shallow-renderer "^16.13.1" scheduler "^0.20.1" +react-transition-group@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" + integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"