From 672675b8fae7ba7e9c01cfec3826ae75cdec0564 Mon Sep 17 00:00:00 2001 From: andriiko Date: Mon, 30 Nov 2020 16:19:26 +0200 Subject: [PATCH] add HW4 --- package.json | 1 + src/components/basket/basket.js | 13 +++---- src/components/menu/menu.js | 6 +--- src/components/product/product.js | 13 +++---- src/components/restaurant/restaurant.js | 36 +++++++++---------- src/components/restaurants/restaurants.js | 5 +-- .../reviews/review-form/review-form.js | 15 +++++--- src/components/reviews/review/review.js | 20 ++++++----- src/components/reviews/reviews.js | 15 ++++---- src/redux/actions.js | 8 ++++- src/redux/constants.js | 1 + src/redux/middleware/generateId.js | 13 +++++++ src/redux/reducer/index.js | 2 ++ src/redux/reducer/products.js | 10 ++---- src/redux/reducer/restaurants.js | 20 ++++++++--- src/redux/reducer/reviews.js | 16 ++++++--- src/redux/reducer/users.js | 21 +++++++++++ src/redux/selectors.js | 34 +++++++++++++++++- src/redux/store.js | 9 +++-- src/redux/utils.js | 11 ++++++ yarn.lock | 2 +- 21 files changed, 185 insertions(+), 86 deletions(-) create mode 100644 src/redux/middleware/generateId.js create mode 100644 src/redux/reducer/users.js create mode 100644 src/redux/utils.js diff --git a/package.json b/package.json index 41f66a5..975dc84 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "redux": "^4.0.5", "redux-devtools-extension": "^2.13.8", "reselect": "^4.0.0", + "uuid": "^8.3.1", "web-vitals": "^0.2.4" }, "scripts": { diff --git a/src/components/basket/basket.js b/src/components/basket/basket.js index 10e0217..53ef8dd 100644 --- a/src/components/basket/basket.js +++ b/src/components/basket/basket.js @@ -1,5 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; +import { createStructuredSelector } from 'reselect'; import styles from './basket.module.css'; import BasketRow from './basket-row'; @@ -38,9 +39,9 @@ function Basket({ title = 'Basket', total, orderProducts }) { ); } -export default connect((state) => { - return { - total: totalSelector(state), - orderProducts: orderProductsSelector(state), - }; -})(Basket); +const mapStateToProps = createStructuredSelector({ + total: totalSelector, + orderProducts: orderProductsSelector, +}); + +export default connect(mapStateToProps)(Basket); diff --git a/src/components/menu/menu.js b/src/components/menu/menu.js index 7935465..ff13c50 100644 --- a/src/components/menu/menu.js +++ b/src/components/menu/menu.js @@ -7,11 +7,7 @@ import styles from './menu.module.css'; class Menu extends React.Component { static propTypes = { - menu: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - }).isRequired - ).isRequired, + menu: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, }; state = { error: null }; diff --git a/src/components/product/product.js b/src/components/product/product.js index 327a6d6..81a304f 100644 --- a/src/components/product/product.js +++ b/src/components/product/product.js @@ -1,11 +1,13 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { createStructuredSelector } from 'reselect'; import styles from './product.module.css'; import { decrement, increment } from '../../redux/actions'; import Button from '../button'; +import { productAmountSelector, productSelector } from '../../redux/selectors'; const Product = ({ product, amount, increment, decrement, fetchData }) => { useEffect(() => { @@ -49,16 +51,11 @@ Product.propTypes = { increment: PropTypes.func, }; -const mapStateToProps = (state, ownProps) => ({ - amount: state.order[ownProps.id] || 0, - product: state.products[ownProps.id], +const mapStateToProps = createStructuredSelector({ + amount: productAmountSelector, + product: productSelector, }); -// const mapDispatchToProps = { -// increment, -// decrement, -// }; - const mapDispatchToProps = (dispatch, ownProps) => ({ increment: () => dispatch(increment(ownProps.id)), decrement: () => dispatch(decrement(ownProps.id)), diff --git a/src/components/restaurant/restaurant.js b/src/components/restaurant/restaurant.js index 3c0d586..396494a 100644 --- a/src/components/restaurant/restaurant.js +++ b/src/components/restaurant/restaurant.js @@ -1,22 +1,20 @@ -import React, { useMemo } from 'react'; +import React from 'react'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import Menu from '../menu'; import Reviews from '../reviews'; import Banner from '../banner'; import Rate from '../rate'; import Tabs from '../tabs'; +import { averageRatingSelector } from '../../redux/selectors'; -const Restaurant = ({ restaurant }) => { - const { name, menu, reviews } = restaurant; - - const averageRating = useMemo(() => { - const total = reviews.reduce((acc, { rating }) => acc + rating, 0); - return Math.round(total / reviews.length); - }, [reviews]); - +const Restaurant = ({ id, name, menu, reviews, averageRating }) => { const tabs = [ { title: 'Menu', content: }, - { title: 'Reviews', content: }, + { + title: 'Reviews', + content: , + }, ]; return ( @@ -30,15 +28,13 @@ const Restaurant = ({ restaurant }) => { }; Restaurant.propTypes = { - restaurant: PropTypes.shape({ - name: PropTypes.string, - menu: PropTypes.array, - reviews: PropTypes.arrayOf( - PropTypes.shape({ - rating: PropTypes.number.isRequired, - }).isRequired - ).isRequired, - }).isRequired, + id: PropTypes.string, + name: PropTypes.string, + menu: PropTypes.array, + reviews: PropTypes.array, + averageRating: PropTypes.number, }; -export default Restaurant; +export default connect((state, props) => ({ + averageRating: averageRatingSelector(state, props), +}))(Restaurant); diff --git a/src/components/restaurants/restaurants.js b/src/components/restaurants/restaurants.js index 1e20ee0..4175e55 100644 --- a/src/components/restaurants/restaurants.js +++ b/src/components/restaurants/restaurants.js @@ -3,11 +3,12 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import Restaurant from '../restaurant'; import Tabs from '../tabs'; +import { restaurantsListSelector } from '../../redux/selectors'; const Restaurants = ({ restaurants }) => { const tabs = restaurants.map((restaurant) => ({ title: restaurant.name, - content: , + content: , })); return ; @@ -22,5 +23,5 @@ Restaurants.propTypes = { }; export default connect((state) => ({ - restaurants: state.restaurants, + restaurants: restaurantsListSelector(state), }))(Restaurants); diff --git a/src/components/reviews/review-form/review-form.js b/src/components/reviews/review-form/review-form.js index 654c421..4c5153a 100644 --- a/src/components/reviews/review-form/review-form.js +++ b/src/components/reviews/review-form/review-form.js @@ -1,12 +1,14 @@ import React from 'react'; +import PropTypes from 'prop-types'; import useForm from '../../../hooks/use-form'; import Rate from '../../rate'; import styles from './review-form.module.css'; import { connect } from 'react-redux'; import Button from '../../button'; +import { addReview } from '../../../redux/actions'; -const INITIAL_VALUES = { name: '', text: '', rate: 5 }; +const INITIAL_VALUES = { name: '', text: '', rating: 5 }; const ReviewForm = ({ onSubmit }) => { const { values, handlers, reset } = useForm(INITIAL_VALUES); @@ -38,7 +40,7 @@ const ReviewForm = ({ onSubmit }) => {
Rating: - +
@@ -51,6 +53,11 @@ const ReviewForm = ({ onSubmit }) => { ); }; -export default connect(null, () => ({ - onSubmit: (values) => console.log(values), // TODO +ReviewForm.propTypes = { + restaurantId: PropTypes.string, + 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 b35f517..c8777f7 100644 --- a/src/components/reviews/review/review.js +++ b/src/components/reviews/review/review.js @@ -3,8 +3,10 @@ import PropTypes from 'prop-types'; import Rate from '../../rate'; import styles from './review.module.css'; +import { connect } from 'react-redux'; +import { reviewWitUserSelector } from '../../../redux/selectors'; -const Review = ({ user, text, rating }) => ( +const Review = ({ review: { user = 'Anonymous', text, rating } }) => (
@@ -23,13 +25,13 @@ const Review = ({ user, text, rating }) => ( ); Review.propTypes = { - user: PropTypes.string, - text: PropTypes.string, - rating: PropTypes.number.isRequired, + review: PropTypes.shape({ + user: PropTypes.string, + text: PropTypes.string, + rating: PropTypes.number.isRequired, + }), }; -Review.defaultProps = { - user: 'Anonymous', -}; - -export default Review; +export default connect((state, props) => ({ + review: reviewWitUserSelector(state, props), +}))(Review); diff --git a/src/components/reviews/reviews.js b/src/components/reviews/reviews.js index 0fdbf58..d9fce05 100644 --- a/src/components/reviews/reviews.js +++ b/src/components/reviews/reviews.js @@ -4,23 +4,20 @@ import Review from './review'; import ReviewForm from './review-form'; import styles from './reviews.module.css'; -const Reviews = ({ reviews }) => { +const Reviews = ({ reviews, restaurantId }) => { return (
- {reviews.map((review) => ( - + {reviews.map((id) => ( + ))} - +
); }; Reviews.propTypes = { - reviews: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - }).isRequired - ).isRequired, + restaurantId: PropTypes.string.isRequired, + reviews: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, }; export default Reviews; diff --git a/src/redux/actions.js b/src/redux/actions.js index 1403ee9..5073fb7 100644 --- a/src/redux/actions.js +++ b/src/redux/actions.js @@ -1,5 +1,11 @@ -import { INCREMENT, DECREMENT, REMOVE } from './constants'; +import { INCREMENT, DECREMENT, REMOVE, ADD_REVIEW } from './constants'; 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 addReview = (review, restaurantId) => ({ + type: ADD_REVIEW, + payload: { review, restaurantId }, + generateId: ['reviewId', 'userId'], +}); 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/generateId.js b/src/redux/middleware/generateId.js new file mode 100644 index 0000000..46668f4 --- /dev/null +++ b/src/redux/middleware/generateId.js @@ -0,0 +1,13 @@ +import { v4 as uuid } from 'uuid'; + +const generateId = (store) => (next) => (action) => { + if (!action.generateId) return next(action); + + const { generateId, ...rest } = action; + next({ + ...rest, + ...generateId.reduce((acc, key) => ({ ...acc, [key]: uuid() }), {}), + }); +}; + +export default generateId; diff --git a/src/redux/reducer/index.js b/src/redux/reducer/index.js index af5bd9c..c064aae 100644 --- a/src/redux/reducer/index.js +++ b/src/redux/reducer/index.js @@ -4,12 +4,14 @@ import order from './order'; import restaurants from './restaurants'; import products from './products'; import reviews from './reviews'; +import users from './users'; const reducer = combineReducers({ order, restaurants, products, reviews, + users, }); export default reducer; diff --git a/src/redux/reducer/products.js b/src/redux/reducer/products.js index 317b132..2e08280 100644 --- a/src/redux/reducer/products.js +++ b/src/redux/reducer/products.js @@ -1,17 +1,13 @@ import { normalizedProducts } from '../../fixtures'; - -const defaultProducts = normalizedProducts.reduce( - (acc, product) => ({ ...acc, [product.id]: product }), - {} -); +import { arrToMap } from '../utils'; // { [productId]: product } -const reducer = (products = defaultProducts, action) => { +const reducer = (state = arrToMap(normalizedProducts), action) => { const { type } = action; switch (type) { default: - return products; + return state; } }; diff --git a/src/redux/reducer/restaurants.js b/src/redux/reducer/restaurants.js index 74ec69a..9746608 100644 --- a/src/redux/reducer/restaurants.js +++ b/src/redux/reducer/restaurants.js @@ -1,11 +1,23 @@ -import { normalizedRestaurants as defaultRestaurants } from '../../fixtures'; +import { ADD_REVIEW } from '../constants'; +import { normalizedRestaurants } from '../../fixtures'; +import { arrToMap } from '../utils'; -const reducer = (restaurants = defaultRestaurants, action) => { - const { type } = action; +const reducer = (state = arrToMap(normalizedRestaurants), action) => { + const { type, payload, reviewId } = action; switch (type) { + case ADD_REVIEW: + const restaurant = state[payload.restaurantId]; + return { + ...state, + [payload.restaurantId]: { + ...restaurant, + reviews: [...restaurant.reviews, reviewId], + }, + }; + default: - return restaurants; + return state; } }; diff --git a/src/redux/reducer/reviews.js b/src/redux/reducer/reviews.js index 0d14a13..23c8ebe 100644 --- a/src/redux/reducer/reviews.js +++ b/src/redux/reducer/reviews.js @@ -1,11 +1,19 @@ -import { normalizedReviews as defaultReviews } from '../../fixtures'; +import { ADD_REVIEW } from '../constants'; +import { normalizedReviews } from '../../fixtures'; +import { arrToMap } from '../utils'; -const reducer = (reviews = defaultReviews, action) => { - const { type } = action; +const reducer = (state = arrToMap(normalizedReviews), action) => { + const { type, payload, reviewId, userId } = action; switch (type) { + case ADD_REVIEW: + const { text, rating } = payload.review; + return { + ...state, + [reviewId]: { id: reviewId, userId, text, rating }, + }; default: - return reviews; + return state; } }; diff --git a/src/redux/reducer/users.js b/src/redux/reducer/users.js new file mode 100644 index 0000000..4519ec9 --- /dev/null +++ b/src/redux/reducer/users.js @@ -0,0 +1,21 @@ +import { ADD_REVIEW } from '../constants'; +import { normalizedUsers } from '../../fixtures'; +import { arrToMap } from '../utils'; + +const reducer = (state = arrToMap(normalizedUsers), action) => { + const { type, payload, userId } = action; + + switch (type) { + case ADD_REVIEW: + const { name } = payload.review; + return { + ...state, + [userId]: { id: userId, name }, + }; + + default: + return state; + } +}; + +export default reducer; diff --git a/src/redux/selectors.js b/src/redux/selectors.js index 7f0a211..0a48b30 100644 --- a/src/redux/selectors.js +++ b/src/redux/selectors.js @@ -1,6 +1,7 @@ import { createSelector } from 'reselect'; +import { getById } from './utils'; -// const restaurantsSelector = (state) => state.restaurants; +const restaurantsSelector = (state) => state.restaurants; const orderSelector = (state) => state.order; const productsSelector = (state) => state.products; @@ -24,3 +25,34 @@ export const totalSelector = createSelector( (orderProducts) => orderProducts.reduce((acc, { subtotal }) => acc + subtotal, 0) ); + +const reviewsSelector = (state) => state.reviews; +const usersSelector = (state) => state.users; + +export const restaurantsListSelector = createSelector( + restaurantsSelector, + Object.values +); +export const productAmountSelector = getById(orderSelector, 0); +export const productSelector = getById(productsSelector); +const reviewSelector = getById(reviewsSelector); + +export const reviewWitUserSelector = createSelector( + reviewSelector, + usersSelector, + (review, users) => ({ + ...review, + user: users[review.userId]?.name, + }) +); + +export const averageRatingSelector = createSelector( + reviewsSelector, + (_, { reviews }) => reviews, + (reviews, ids) => { + const ratings = ids.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 7988b58..cebe35b 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -1,12 +1,11 @@ import { applyMiddleware, createStore } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; + import logger from './middleware/logger'; +import generateId from './middleware/generateId'; import reducer from './reducer'; -const store = createStore( - reducer, - composeWithDevTools(applyMiddleware(logger)) -); +const enhancer = applyMiddleware(generateId, logger); -export default store; +export default createStore(reducer, composeWithDevTools(enhancer)); diff --git a/src/redux/utils.js b/src/redux/utils.js new file mode 100644 index 0000000..03f5df7 --- /dev/null +++ b/src/redux/utils.js @@ -0,0 +1,11 @@ +import { createSelector } from 'reselect'; + +export const arrToMap = (arr) => + arr.reduce((acc, item) => ({ ...acc, [item.id]: item }), {}); + +export const getById = (selector, defaultValue) => + createSelector( + selector, + (_, props) => props.id, + (entity, id) => entity[id] || defaultValue + ); diff --git a/yarn.lock b/yarn.lock index af7cd76..7ed90e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11256,7 +11256,7 @@ uuid@^3.3.2, uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.0: +uuid@^8.3.0, uuid@^8.3.1: version "8.3.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==