From 7830ddde90d1041a6794ff423ff095f7c734cea3 Mon Sep 17 00:00:00 2001 From: Vera Lobacheva Date: Tue, 14 Dec 2021 01:16:23 +0300 Subject: [PATCH] HW-4 --- package.json | 3 +- src/components/menu/menu.js | 6 +--- src/components/product/product.js | 5 +-- src/components/restaurant/restaurant.js | 35 +++++++++++-------- src/components/restaurants/restaurants.js | 14 ++++---- .../reviews/review-form/review-form.js | 14 ++++++-- src/components/reviews/review/review.js | 7 +++- src/components/reviews/reviews.js | 15 ++++---- src/redux/actions.js | 9 ++++- src/redux/constants.js | 1 + src/redux/middleware/uuid.js | 10 ++++++ src/redux/reducer/index.js | 2 ++ src/redux/reducer/restaurants.js | 20 +++++++++-- src/redux/reducer/reviews.js | 17 +++++++-- src/redux/reducer/users.js | 23 ++++++++++++ src/redux/selectors.js | 35 ++++++++++++++++++- src/redux/store.js | 3 +- 17 files changed, 168 insertions(+), 51 deletions(-) create mode 100644 src/redux/middleware/uuid.js create mode 100644 src/redux/reducer/users.js diff --git a/package.json b/package.json index 2d2dfb0..cc14b67 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "react-scripts": "4.0.3", "redux": "^4.1.2", "redux-devtools-extension": "^2.13.9", - "reselect": "^4.1.5" + "reselect": "^4.1.5", + "uuid": "^8.3.2" }, "scripts": { "start": "react-scripts start", diff --git a/src/components/menu/menu.js b/src/components/menu/menu.js index aebcf81..9acea29 100644 --- a/src/components/menu/menu.js +++ b/src/components/menu/menu.js @@ -8,11 +8,7 @@ import styles from './menu.module.css'; class Menu extends Component { static propTypes = { - menu: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - }).isRequired - ).isRequired, + menu: PropTypes.arrayOf(PropTypes.string).isRequired, }; state = { error: null }; diff --git a/src/components/product/product.js b/src/components/product/product.js index 90f671f..0f9ab67 100644 --- a/src/components/product/product.js +++ b/src/components/product/product.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import styles from './product.module.css'; import Button from '../button'; import { decrement, increment } from '../../redux/actions'; +import { amountSelector, productSelector } from '../../redux/selectors'; function Product({ product, amount, decrement, increment, fetchData }) { useEffect(() => { @@ -57,8 +58,8 @@ Product.propTypes = { }; const mapStateToProps = (state, props) => ({ - amount: state.order[props.id] || 0, - product: state.products[props.id], + amount: amountSelector(state, props), + product: productSelector(state, props), }); // const mapDispatchToProps = { diff --git a/src/components/restaurant/restaurant.js b/src/components/restaurant/restaurant.js index 364e73c..cd74f7b 100644 --- a/src/components/restaurant/restaurant.js +++ b/src/components/restaurant/restaurant.js @@ -1,4 +1,5 @@ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import Menu from '../menu'; import Reviews from '../reviews'; @@ -6,16 +7,16 @@ import Banner from '../banner'; import Rate from '../rate'; import Tabs from '../tabs'; -const Restaurant = ({ restaurant }) => { +import { + averageRatingSelector, + restaurantSelector, +} from '../../redux/selectors'; + +const Restaurant = ({ restaurant, averageRating }) => { const { id, name, menu, reviews } = restaurant; const [activeTab, setActiveTab] = useState('menu'); - const averageRating = useMemo(() => { - const total = reviews.reduce((acc, { rating }) => acc + rating, 0); - return Math.round(total / reviews.length); - }, [reviews]); - const tabs = [ { id: 'menu', label: 'Menu' }, { id: 'reviews', label: 'Reviews' }, @@ -28,7 +29,9 @@ const Restaurant = ({ restaurant }) => { {activeTab === 'menu' && } - {activeTab === 'reviews' && } + {activeTab === 'reviews' && ( + + )} ); }; @@ -37,13 +40,15 @@ Restaurant.propTypes = { restaurant: PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string, - menu: PropTypes.array, - reviews: PropTypes.arrayOf( - PropTypes.shape({ - rating: PropTypes.number.isRequired, - }).isRequired - ).isRequired, + menu: PropTypes.arrayOf(PropTypes.string), + reviews: PropTypes.arrayOf(PropTypes.string).isRequired, + averageRating: PropTypes.number, }).isRequired, }; -export default Restaurant; +const mapStateToProps = (state, props) => ({ + restaurant: restaurantSelector(state, props), + averageRating: averageRatingSelector(state, props), +}); + +export default connect(mapStateToProps)(Restaurant); diff --git a/src/components/restaurants/restaurants.js b/src/components/restaurants/restaurants.js index 2037758..fdb71de 100644 --- a/src/components/restaurants/restaurants.js +++ b/src/components/restaurants/restaurants.js @@ -1,9 +1,12 @@ import { useMemo, useState } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; + import Restaurant from '../restaurant'; import Tabs from '../tabs'; +import { restaurantsAsArraySelector } from '../../redux/selectors'; + function Restaurants({ restaurants }) { const [activeId, setActiveId] = useState(restaurants[0].id); @@ -12,15 +15,10 @@ function Restaurants({ restaurants }) { [restaurants] ); - const activeRestaurant = useMemo( - () => restaurants.find((restaurant) => restaurant.id === activeId), - [activeId, restaurants] - ); - return (
- +
); } @@ -30,12 +28,12 @@ Restaurants.propTypes = { PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string, - }).isRequired + }) ).isRequired, }; const mapStateToProps = (state) => ({ - restaurants: state.restaurants, + restaurants: restaurantsAsArraySelector(state), }); export default connect(mapStateToProps)(Restaurants); diff --git a/src/components/reviews/review-form/review-form.js b/src/components/reviews/review-form/review-form.js index 1267e77..6d14fef 100644 --- a/src/components/reviews/review-form/review-form.js +++ b/src/components/reviews/review-form/review-form.js @@ -1,14 +1,17 @@ import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; import useForm from '../../../hooks/use-form'; import Rate from '../../rate'; import Button from '../../button'; +import { addReview } from '../../../redux/actions'; + import styles from './review-form.module.css'; const INITIAL_VALUES = { name: '', text: '', rating: 3 }; -const ReviewForm = ({ onSubmit }) => { +const ReviewForm = ({ restaurantId, onSubmit }) => { const { values, handlers, reset } = useForm(INITIAL_VALUES); const handleSubmit = (ev) => { @@ -51,6 +54,11 @@ const ReviewForm = ({ onSubmit }) => { ); }; -export default connect(null, () => ({ - onSubmit: (values) => console.log(values), // TODO +ReviewForm.propTypes = { + restaurantId: PropTypes.string.isRequired, + onSubmit: PropTypes.func.isRequired, +}; + +export default connect(null, (dispatch, props) => ({ + onSubmit: (review) => dispatch(addReview(review, props.restaurantId)), }))(ReviewForm); diff --git a/src/components/reviews/review/review.js b/src/components/reviews/review/review.js index fe89ecb..987076d 100644 --- a/src/components/reviews/review/review.js +++ b/src/components/reviews/review/review.js @@ -1,4 +1,7 @@ import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { reviewFullInfoSelector } from '../../../redux/selectors'; import Rate from '../../rate'; import styles from './review.module.css'; @@ -31,4 +34,6 @@ Review.defaultProps = { user: 'Anonymous', }; -export default Review; +export default connect((state, props) => reviewFullInfoSelector(state, props))( + Review +); diff --git a/src/components/reviews/reviews.js b/src/components/reviews/reviews.js index 66f6cf1..ce84bc8 100644 --- a/src/components/reviews/reviews.js +++ b/src/components/reviews/reviews.js @@ -3,23 +3,20 @@ import Review from './review'; import ReviewForm from './review-form'; import styles from './reviews.module.css'; -const Reviews = ({ reviews }) => { +const Reviews = ({ restaurantId, reviewIds }) => { return (
- {reviews.map((review) => ( - + {reviewIds.map((reviewId) => ( + ))} - +
); }; Reviews.propTypes = { - reviews: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - }).isRequired - ).isRequired, + restaurantId: PropTypes.string, + reviewIds: PropTypes.arrayOf(PropTypes.string).isRequired, }; export default Reviews; diff --git a/src/redux/actions.js b/src/redux/actions.js index ea78f99..405bfd1 100644 --- a/src/redux/actions.js +++ b/src/redux/actions.js @@ -1,5 +1,12 @@ -import { DECREMENT, INCREMENT, REMOVE } from './constants'; +import { DECREMENT, INCREMENT, REMOVE, ADD_REVIEW } from './constants'; export const increment = (id) => ({ type: INCREMENT, id }); export const decrement = (id) => ({ type: DECREMENT, id }); export const remove = (id) => ({ type: REMOVE, id }); + +export const addReview = (review, restaurantId) => ({ + type: ADD_REVIEW, + review, + restaurantId, + uuidFor: ['userId', 'reviewId'], +}); diff --git a/src/redux/constants.js b/src/redux/constants.js index 9cfa25d..aaac693 100644 --- a/src/redux/constants.js +++ b/src/redux/constants.js @@ -1,3 +1,4 @@ export const INCREMENT = 'INCREMENT'; export const DECREMENT = 'DECREMENT'; export const REMOVE = 'REMOVE'; +export const ADD_REVIEW = 'ADD_REVIEW'; diff --git a/src/redux/middleware/uuid.js b/src/redux/middleware/uuid.js new file mode 100644 index 0000000..f542030 --- /dev/null +++ b/src/redux/middleware/uuid.js @@ -0,0 +1,10 @@ +import { v4 as uuid } from 'uuid'; + +export default (store) => (next) => (action) => { + if (!action.uuidFor) return next(action); + const { uuidFor, ...rest } = action; + next({ + ...rest, + ...uuidFor.reduce((acc, key) => ({ ...acc, [key]: uuid() }), {}), + }); +}; diff --git a/src/redux/reducer/index.js b/src/redux/reducer/index.js index f86f67d..3b5c632 100644 --- a/src/redux/reducer/index.js +++ b/src/redux/reducer/index.js @@ -3,10 +3,12 @@ import order from './order'; import restaurants from './restaurants'; import products from './products'; import reviews from './reviews'; +import users from './users'; export default combineReducers({ order, restaurants, products, reviews, + users, }); diff --git a/src/redux/reducer/restaurants.js b/src/redux/reducer/restaurants.js index e7f30c6..10f8e22 100644 --- a/src/redux/reducer/restaurants.js +++ b/src/redux/reducer/restaurants.js @@ -1,9 +1,25 @@ -import { normalizedRestaurants as defaultRestaurants } from '../../fixtures'; +import { normalizedRestaurants } from '../../fixtures'; +import { ADD_REVIEW } from '../constants'; + +const defaultRestaurants = normalizedRestaurants.reduce( + (acc, restaurant) => ({ ...acc, [restaurant.id]: restaurant }), + {} +); export default (restaurants = defaultRestaurants, action) => { - const { type } = action; + const { type, restaurantId, reviewId } = action; switch (type) { + case ADD_REVIEW: + const restaurant = restaurants[restaurantId]; + return { + ...restaurants, + [restaurantId]: { + ...restaurant, + reviews: [...restaurant.reviews, reviewId], + }, + }; + default: return restaurants; } diff --git a/src/redux/reducer/reviews.js b/src/redux/reducer/reviews.js index 494b5cd..e3600ef 100644 --- a/src/redux/reducer/reviews.js +++ b/src/redux/reducer/reviews.js @@ -1,9 +1,22 @@ -import { normalizedReviews as defaultReviews } from '../../fixtures'; +import { normalizedReviews } from '../../fixtures'; +import { ADD_REVIEW } from '../constants'; + +const defaultReviews = normalizedReviews.reduce( + (acc, review) => ({ ...acc, [review.id]: review }), + {} +); export default (reviews = defaultReviews, action) => { - const { type } = action; + const { type, userId, reviewId, review } = action; switch (type) { + case ADD_REVIEW: + const { text, rating } = review; + return { + ...reviews, + [reviewId]: { userId, id: reviewId, text, rating }, + }; + default: return reviews; } diff --git a/src/redux/reducer/users.js b/src/redux/reducer/users.js new file mode 100644 index 0000000..716542c --- /dev/null +++ b/src/redux/reducer/users.js @@ -0,0 +1,23 @@ +import { normalizedUsers } from '../../fixtures'; +import { ADD_REVIEW } from '../constants'; + +const defaultUsers = normalizedUsers.reduce( + (acc, user) => ({ ...acc, [user.id]: user }), + {} +); + +export default (users = defaultUsers, action) => { + const { type, review, userId } = action; + + switch (type) { + case ADD_REVIEW: + const { name } = review; + return { + ...users, + [userId]: { id: userId, name }, + }; + + default: + return users; + } +}; diff --git a/src/redux/selectors.js b/src/redux/selectors.js index 11b67eb..4aaf189 100644 --- a/src/redux/selectors.js +++ b/src/redux/selectors.js @@ -1,8 +1,10 @@ import { createSelector } from 'reselect'; -// const restaurantsSelector = (state) => state.restaurants; +const restaurantsSelector = (state) => state.restaurants; const productsSelector = (state) => state.products; const orderSelector = (state) => state.order; +const reviewsSelector = (state) => state.reviews; +const usersSelector = (state) => state.users; export const orderProductsSelector = createSelector( [productsSelector, orderSelector], @@ -22,3 +24,34 @@ export const totalSelector = createSelector( (orderProducts) => orderProducts.reduce((acc, { subtotal }) => acc + subtotal, 0) ); + +export const amountSelector = (state, { id }) => orderSelector(state)[id] || 0; +export const productSelector = (state, { id }) => productsSelector(state)[id]; +export const restaurantSelector = (state, { id }) => + restaurantsSelector(state)[id]; + +export const restaurantsAsArraySelector = createSelector( + [restaurantsSelector], + (restaurants) => Object.values(restaurants) +); + +export const reviewSelector = (state, { id }) => reviewsSelector(state)[id]; + +export const reviewFullInfoSelector = createSelector( + [reviewSelector, usersSelector], + (review, users) => ({ + ...review, + user: users[review.userId]?.name, + }) +); + +export const averageRatingSelector = createSelector( + reviewsSelector, + restaurantSelector, + (reviews, restaurant) => { + const ratings = restaurant.reviews.map((id) => reviews[id].rating); + return Math.round( + ratings.reduce((acc, rating) => acc + rating) / ratings.length + ); + } +); diff --git a/src/redux/store.js b/src/redux/store.js index 4ec2265..8c752c3 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -1,10 +1,11 @@ import { applyMiddleware, createStore } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; import logger from './middleware/logger'; +import uuid from './middleware/uuid'; import reducer from './reducer'; export default createStore( reducer, - composeWithDevTools(applyMiddleware(logger)) + composeWithDevTools(applyMiddleware(uuid, logger)) );