From aa306719f65dea833596b2b9b44bc15277eab274 Mon Sep 17 00:00:00 2001 From: Mm Date: Tue, 26 Oct 2021 05:20:55 +0300 Subject: [PATCH] hw4: add redusers, selectors, new review adding from form --- 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 | 34 +++++++--------- .../reviews/review-form/review-form.js | 20 +++++++--- src/components/reviews/review/review.js | 10 ++++- src/components/reviews/reviews.js | 14 +++---- src/redux/actions.js | 3 +- src/redux/constants.js | 1 + src/redux/middleware/uuidGenerator.js | 7 ++++ src/redux/reducer/index.js | 2 + src/redux/reducer/restaurants.js | 20 +++++++++- src/redux/reducer/reviews.js | 13 ++++++- src/redux/reducer/users.js | 20 ++++++++++ src/redux/selectors.js | 39 ++++++++++++++++++- src/redux/store.js | 3 +- 17 files changed, 167 insertions(+), 68 deletions(-) create mode 100644 src/redux/middleware/uuidGenerator.js create mode 100644 src/redux/reducer/users.js diff --git a/package.json b/package.json index ba1b4a1..21dae21 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "react-scripts": "4.0.3", "redux": "^4.1.1", "redux-devtools-extension": "^2.13.9", - "reselect": "^4.0.0" + "reselect": "^4.0.0", + "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 71ce160..2d7d60f 100644 --- a/src/components/product/product.js +++ b/src/components/product/product.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import styles from './product.module.css'; import Button from '../button'; import { decrement, increment } from '../../redux/actions'; +import { orderSelectorById, productSelectorById } from '../../redux/selectors'; function Product({ product, amount, decrement, increment, fetchData }) { useEffect(() => { @@ -56,8 +57,8 @@ Product.propTypes = { }; const mapStateToProps = (state, props) => ({ - amount: state.order[props.id] || 0, - product: state.products[props.id], + amount: orderSelectorById(state, props), + product: productSelectorById(state, props), }); // const mapDispatchToProps = { diff --git a/src/components/restaurant/restaurant.js b/src/components/restaurant/restaurant.js index 364e73c..f9e329a 100644 --- a/src/components/restaurant/restaurant.js +++ b/src/components/restaurant/restaurant.js @@ -1,21 +1,21 @@ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; import Menu from '../menu'; import Reviews from '../reviews'; import Banner from '../banner'; import Rate from '../rate'; import Tabs from '../tabs'; +import { + restaurantSelectorById, + averageRatingSelector, +} from '../../redux/selectors'; -const Restaurant = ({ restaurant }) => { +const Restaurant = ({ restaurantId, 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,22 +28,27 @@ const Restaurant = ({ restaurant }) => { {activeTab === 'menu' && } - {activeTab === 'reviews' && } + {activeTab === 'reviews' && ( + + )} ); }; Restaurant.propTypes = { + restaurantId: PropTypes.string.isRequired, 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, }; -export default Restaurant; +const mapStateToProps = (state, props) => ({ + restaurant: restaurantSelectorById(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..cc8778e 100644 --- a/src/components/restaurants/restaurants.js +++ b/src/components/restaurants/restaurants.js @@ -1,41 +1,33 @@ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import Restaurant from '../restaurant'; import Tabs from '../tabs'; +import { tabsSelector } from '../../redux/selectors'; -function Restaurants({ restaurants }) { - const [activeId, setActiveId] = useState(restaurants[0].id); - - const tabs = useMemo( - () => restaurants.map(({ id, name }) => ({ id, label: name })), - [restaurants] - ); - - const activeRestaurant = useMemo( - () => restaurants.find((restaurant) => restaurant.id === activeId), - [activeId, restaurants] - ); +function Restaurants({ tabs }) { + const [activeId, setActiveId] = useState(tabs[0].id); return (
- +
); } Restaurants.propTypes = { - restaurants: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string, - }).isRequired - ).isRequired, + tabs: PropTypes.arrayOf( + PropTypes.shape( + { + id: PropTypes.string.isRequired, + }.isRequired + ).isRequired + ), }; const mapStateToProps = (state) => ({ - restaurants: state.restaurants, + tabs: tabsSelector(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..5dbef60 100644 --- a/src/components/reviews/review-form/review-form.js +++ b/src/components/reviews/review-form/review-form.js @@ -1,14 +1,14 @@ 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 styles from './review-form.module.css'; +import { addreview } from '../../../redux/actions'; -const INITIAL_VALUES = { name: '', text: '', rating: 3 }; - -const ReviewForm = ({ onSubmit }) => { +const ReviewForm = ({ restaurantId, onSubmit }) => { + const INITIAL_VALUES = { name: '', text: '', rating: 3, restaurantId }; const { values, handlers, reset } = useForm(INITIAL_VALUES); const handleSubmit = (ev) => { @@ -51,6 +51,14 @@ const ReviewForm = ({ onSubmit }) => { ); }; -export default connect(null, () => ({ - onSubmit: (values) => console.log(values), // TODO +ReviewForm.propTypes = { + restaurantId: PropTypes.string.isRequired, + onSubmit: PropTypes.func, +}; + +export default connect(null, (dispatch) => ({ + onSubmit: (values) => { + console.log(values); // TODO + dispatch(addreview(values)); + }, }))(ReviewForm); diff --git a/src/components/reviews/review/review.js b/src/components/reviews/review/review.js index fe89ecb..a7d8cc9 100644 --- a/src/components/reviews/review/review.js +++ b/src/components/reviews/review/review.js @@ -1,7 +1,8 @@ import PropTypes from 'prop-types'; - +import { connect } from 'react-redux'; import Rate from '../../rate'; import styles from './review.module.css'; +import { reviewSelectorById } from '../../../redux/selectors'; const Review = ({ user, text, rating }) => (
@@ -31,4 +32,9 @@ Review.defaultProps = { user: 'Anonymous', }; -export default Review; +const mapStateToProps = (state, props) => { + const { user, text, rating } = reviewSelectorById(state, props); + return { user, text, rating }; +}; + +export default connect(mapStateToProps)(Review); diff --git a/src/components/reviews/reviews.js b/src/components/reviews/reviews.js index 66f6cf1..b95520b 100644 --- a/src/components/reviews/reviews.js +++ b/src/components/reviews/reviews.js @@ -3,23 +3,19 @@ import Review from './review'; import ReviewForm from './review-form'; import styles from './reviews.module.css'; -const Reviews = ({ reviews }) => { +const Reviews = ({ restaurantId, reviews }) => { return (
- {reviews.map((review) => ( - + {reviews.map((id) => ( + ))} - +
); }; Reviews.propTypes = { - reviews: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - }).isRequired - ).isRequired, + reviews: PropTypes.arrayOf(PropTypes.string).isRequired, }; export default Reviews; diff --git a/src/redux/actions.js b/src/redux/actions.js index ea78f99..3d12ae7 100644 --- a/src/redux/actions.js +++ b/src/redux/actions.js @@ -1,5 +1,6 @@ -import { DECREMENT, INCREMENT, REMOVE } from './constants'; +import { DECREMENT, INCREMENT, REMOVE, ADDREVIEW } 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) => ({ type: ADDREVIEW, review }); diff --git a/src/redux/constants.js b/src/redux/constants.js index 9cfa25d..b811296 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 ADDREVIEW = 'ADDREVIEW'; diff --git a/src/redux/middleware/uuidGenerator.js b/src/redux/middleware/uuidGenerator.js new file mode 100644 index 0000000..5392266 --- /dev/null +++ b/src/redux/middleware/uuidGenerator.js @@ -0,0 +1,7 @@ +import { v4 as uuidv4 } from 'uuid'; + +export default (store) => (next) => (action) => { + action.review.id = uuidv4(); + action.review.userId = uuidv4(); + next(action); +}; 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..17b8f93 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 { ADDREVIEW } from '../constants'; + +const defaultRestaurants = normalizedRestaurants.reduce( + (acc, restaurant) => ({ ...acc, [restaurant.id]: restaurant }), + {} +); export default (restaurants = defaultRestaurants, action) => { - const { type } = action; + const { type, review } = action; switch (type) { + case ADDREVIEW: + const { restaurantId, id: reviewId } = 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..4ca0d70 100644 --- a/src/redux/reducer/reviews.js +++ b/src/redux/reducer/reviews.js @@ -1,9 +1,18 @@ -import { normalizedReviews as defaultReviews } from '../../fixtures'; +import { normalizedReviews } from '../../fixtures'; +import { ADDREVIEW } from '../constants'; + +const defaultReviews = normalizedReviews.reduce( + (acc, rewiew) => ({ ...acc, [rewiew.id]: rewiew }), + {} +); export default (reviews = defaultReviews, action) => { - const { type } = action; + const { type, review } = action; switch (type) { + case ADDREVIEW: + const { id, userId, text, rating } = review; + return { ...reviews, [id]: { id, userId, 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..e72a059 --- /dev/null +++ b/src/redux/reducer/users.js @@ -0,0 +1,20 @@ +import { normalizedUsers } from '../../fixtures'; +import { ADDREVIEW } from '../constants'; + +const defaultUsers = normalizedUsers.reduce( + (acc, user) => ({ ...acc, [user.id]: user }), + {} +); + +export default (users = defaultUsers, action) => { + const { type, review } = action; + + switch (type) { + case ADDREVIEW: + const { userId, name } = review; + return { ...users, [userId]: { userId, name } }; + + default: + return users; + } +}; diff --git a/src/redux/selectors.js b/src/redux/selectors.js index 887af3f..aa4110d 100644 --- a/src/redux/selectors.js +++ b/src/redux/selectors.js @@ -1,9 +1,29 @@ import { createSelector } from 'reselect'; -// const restaurantsSelector = (state) => state.restaurants; +const restaurantsSelector = (state) => state.restaurants; const productsSelector = (state) => state.products; const orderSelector = (state) => state.order; +export const restaurantSelectorById = (state, props) => + state.restaurants[props.restaurantId]; + +const reviewsSelectorByRestaurantId = (state, props) => { + return state.restaurants[props.restaurantId].reviews.map( + (reviewId) => state.reviews[reviewId] + ); +}; + +export const orderSelectorById = (state, props) => state.order[props.id] || 0; +export const productSelectorById = (state, props) => state.products[props.id]; +export const reviewSelectorById = (state, props) => { + const review = state.reviews[props.id]; + return { + text: review.text, + rating: review.rating, + user: state.users[review.userId].name || 'Anonymous', + }; +}; + export const orderProductsSelector = createSelector( orderSelector, productsSelector, @@ -23,3 +43,20 @@ export const totalSelector = createSelector( (orderProducts) => orderProducts.reduce((acc, { subtotal }) => acc + subtotal, 0) ); + +export const tabsSelector = createSelector( + [restaurantsSelector], + (restaurants) => + Object.entries(restaurants).map(([id, restaurant]) => ({ + id, + label: restaurant.name, + })) +); + +export const averageRatingSelector = createSelector( + [reviewsSelectorByRestaurantId], + (reviews) => { + const total = reviews.reduce((acc, { rating }) => acc + rating, 0); + return Math.round(total / reviews.length); + } +); diff --git a/src/redux/store.js b/src/redux/store.js index 7ac3e5a..61c72d4 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -2,10 +2,11 @@ import { applyMiddleware, createStore } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; import logger from './middleware/logger'; +import uuidGenerator from './middleware/uuidGenerator'; import reducer from './reducer'; export default createStore( reducer, - composeWithDevTools(applyMiddleware(logger)) + composeWithDevTools(applyMiddleware(logger, uuidGenerator)) );