@@ -18,7 +25,9 @@ const Product = ({ product, amount, increment, decrement }) => {
@@ -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"