From 9e40b0f4a42248afdf6bf6bfe5de00d272d23b8d Mon Sep 17 00:00:00 2001 From: ktvtk Date: Sun, 1 Dec 2024 19:44:43 +0500 Subject: [PATCH 1/2] hw 8.1 --- src/components/cities-list/cities-list.tsx | 19 +++- src/components/header/header.tsx | 74 ++++++++++++++ src/components/offers-list/offers-list.tsx | 4 +- src/components/place-card/place-card.tsx | 57 ++++++++--- .../private-route/private-route.tsx | 5 +- src/components/review-form/review-form.tsx | 17 ++-- src/components/review-item/review-item.tsx | 7 +- src/components/review-list/review-list.tsx | 13 ++- src/components/sorting/sorting.tsx | 17 ++-- src/const.ts | 18 +++- src/index.tsx | 5 +- .../favorites-screen/favorite-screen.tsx | 36 +------ src/pages/login-screen/login-screen.tsx | 23 ++--- src/pages/main-screen/main-screen.tsx | 70 ++----------- .../not-found-screen/not-found-screen.tsx | 2 +- src/pages/offer-screen/offer-screen.tsx | 98 +++++-------------- src/servises/api.ts | 5 +- src/store/action.ts | 19 ---- src/store/api-actions.ts | 66 ++++++++++--- src/store/app-data/app-data.ts | 20 ++++ src/store/app-data/selectors.ts | 5 + .../detail-offer-data/detail-offer-data.ts | 33 +++++++ src/store/detail-offer-data/selectors.ts | 9 ++ src/store/index.ts | 4 +- src/store/offers-data/offers-data.ts | 33 +++++++ src/store/offers-data/selectors.ts | 9 ++ src/store/reducer.ts | 55 ----------- src/store/root-reducer.ts | 13 +++ src/store/user-data/selectors.ts | 6 ++ src/store/user-data/user-data.ts | 23 +++++ src/types/favorite-info.ts | 6 ++ src/types/state.ts | 26 +++++ 32 files changed, 477 insertions(+), 320 deletions(-) create mode 100644 src/components/header/header.tsx delete mode 100644 src/store/action.ts create mode 100644 src/store/app-data/app-data.ts create mode 100644 src/store/app-data/selectors.ts create mode 100644 src/store/detail-offer-data/detail-offer-data.ts create mode 100644 src/store/detail-offer-data/selectors.ts create mode 100644 src/store/offers-data/offers-data.ts create mode 100644 src/store/offers-data/selectors.ts delete mode 100644 src/store/reducer.ts create mode 100644 src/store/root-reducer.ts create mode 100644 src/store/user-data/selectors.ts create mode 100644 src/store/user-data/user-data.ts create mode 100644 src/types/favorite-info.ts diff --git a/src/components/cities-list/cities-list.tsx b/src/components/cities-list/cities-list.tsx index 62caa99..e63d83b 100644 --- a/src/components/cities-list/cities-list.tsx +++ b/src/components/cities-list/cities-list.tsx @@ -1,15 +1,20 @@ -import {JSX} from 'react'; +import {JSX, memo, useCallback} from 'react'; import {Cities} from '../../const.ts'; import {City} from '../../types/city.ts'; import {useAppDispatch} from '../../hooks'; -import {changeActiveCity} from '../../store/action.ts'; +import {changeActiveCity} from '../../store/app-data/app-data.ts'; type CitiesListProps = { activeCity: string; } - -export function CitiesList({activeCity} : CitiesListProps) : JSX.Element { +function CitiesList({activeCity} : CitiesListProps) : JSX.Element { const dispatch = useAppDispatch(); + const changeActiveCityHandle = useCallback( + (city: City) => { + dispatch(changeActiveCity(city)); + }, + [dispatch] + ); return (
@@ -23,7 +28,7 @@ export function CitiesList({activeCity} : CitiesListProps) : JSX.Element { ) : ( dispatch(changeActiveCity(city))} + onClick={() => changeActiveCityHandle(city)} > {city.name} @@ -34,3 +39,7 @@ export function CitiesList({activeCity} : CitiesListProps) : JSX.Element {
); } + +const MemoizedCitiesList = memo(CitiesList); + +export default MemoizedCitiesList; diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx new file mode 100644 index 0000000..dd9e449 --- /dev/null +++ b/src/components/header/header.tsx @@ -0,0 +1,74 @@ +import {JSX, memo, useEffect} from 'react'; +import {Link} from 'react-router-dom'; +import {AppRoute, AuthorizationStatus} from '../../const.ts'; +import {useAppDispatch, useAppSelector} from '../../hooks'; +import {logout} from '../../store/api-actions.ts'; +import {getFavoritesCount} from '../../store/offers-data/selectors.ts'; +import {getAuthoriztionStatus, getUserEmail} from '../../store/user-data/selectors.ts'; +import {saveUserEmail} from '../../store/user-data/user-data.ts'; +function Header() : JSX.Element { + const isAuth = useAppSelector(getAuthoriztionStatus) === AuthorizationStatus.Authorized; + const dispatch = useAppDispatch(); + const logoutHandle = () => { + dispatch(logout()); + }; + const favoritesCount = useAppSelector(getFavoritesCount); + const userEmail = useAppSelector(getUserEmail); + useEffect(() => { + const savedEmail = localStorage.getItem('userEmail'); + if (savedEmail && !userEmail) { + dispatch(saveUserEmail(savedEmail)); + } + }, [dispatch, userEmail]); + + return ( +
+
+
+
+ + 6 cities logo + +
+ {isAuth ? ( + + ) : ( + + )} +
+
+
+ ); +} + +const MemoizedHeader = memo(Header); + +export default MemoizedHeader; diff --git a/src/components/offers-list/offers-list.tsx b/src/components/offers-list/offers-list.tsx index e7142d1..a09f02a 100644 --- a/src/components/offers-list/offers-list.tsx +++ b/src/components/offers-list/offers-list.tsx @@ -1,7 +1,7 @@ import {JSX, useEffect} from 'react'; import {Offer} from '../../types/offer.ts'; import {useState} from 'react'; -import {PlaceCard} from '../place-card/place-card.tsx'; +import MemoizedPlaceCard from '../place-card/place-card.tsx'; type OffersListProps = { offers: Offer[]; @@ -26,7 +26,7 @@ export function OffersList({offers, onChange} : OffersListProps) : JSX.Element { return (
{offers.map((offer) => ( - handleMouseEnter(offer.id)} diff --git a/src/components/place-card/place-card.tsx b/src/components/place-card/place-card.tsx index 14ef7d3..5870f8f 100644 --- a/src/components/place-card/place-card.tsx +++ b/src/components/place-card/place-card.tsx @@ -1,7 +1,11 @@ -import {JSX} from 'react'; +import {JSX, memo, useCallback} from 'react'; import {Offer} from '../../types/offer.ts'; -import {Link} from 'react-router-dom'; -import {AppRoute} from '../../const.ts'; +import {Link, useNavigate} from 'react-router-dom'; +import {AppRoute, AuthorizationStatus, FavoriteStatus} from '../../const.ts'; +import {useAppDispatch, useAppSelector} from '../../hooks'; +import {getAuthoriztionStatus} from '../../store/user-data/selectors.ts'; +import {updateOffers} from '../../store/offers-data/offers-data.ts'; +import {changeFavoriteStatus} from '../../store/api-actions.ts'; type PlaceCardProps = { offer: Offer; @@ -9,45 +13,74 @@ type PlaceCardProps = { onMouseLeave: () => void; } -export function PlaceCard({offer, onMouseLeave, onMouseEnter}: PlaceCardProps): JSX.Element { +function PlaceCard({offer, onMouseLeave, onMouseEnter}: PlaceCardProps): JSX.Element { + const {id, isPremium, previewImage, price, rating, title, type, isFavorite} = offer; + const dispatch = useAppDispatch(); + const isAuth = useAppSelector(getAuthoriztionStatus) === AuthorizationStatus.Authorized; + const navigate = useNavigate(); + + const bookmarkClickHandle = useCallback(() => { + if (isAuth) { + const newStatus = isFavorite ? FavoriteStatus.Remove : FavoriteStatus.Add; + const updatedOffer = { ...offer, isFavorite: !isFavorite }; + dispatch(updateOffers(updatedOffer)); + dispatch(changeFavoriteStatus({ offerId: id, status: newStatus })); + } else { + navigate(AppRoute.Login); + } + }, [isAuth, isFavorite, offer, dispatch, id]); + return (
- {offer.isPremium && + {isPremium &&
Premium
}
- Place image + Place image
- €{offer.price} + €{price} / night
-
- + Rating

- {offer.title} + {title}

-

{offer.type}

+

{type}

); } + +const MemoizedPlaceCard = memo(PlaceCard, (prevProps, nextProps) => { + return prevProps.offer.id === nextProps.offer.id && prevProps.offer.isFavorite === nextProps.offer.isFavorite; +}); +export default MemoizedPlaceCard; diff --git a/src/components/private-route/private-route.tsx b/src/components/private-route/private-route.tsx index 1f43f28..c62a97a 100644 --- a/src/components/private-route/private-route.tsx +++ b/src/components/private-route/private-route.tsx @@ -2,6 +2,7 @@ import {JSX} from 'react'; import {Navigate} from 'react-router-dom'; import {AppRoute, AuthorizationStatus} from '../../const'; import {useAppSelector} from '../../hooks'; +import {getAuthoriztionStatus} from '../../store/user-data/selectors.ts'; type PrivateRouteProps = { children: JSX.Element; @@ -9,7 +10,7 @@ type PrivateRouteProps = { export function PrivateRouteAuthorized(props: PrivateRouteProps): JSX.Element { const {children} = props; - const isAuthorized = useAppSelector((state) => state.authorizationStatus) === AuthorizationStatus.Authorized; + const isAuthorized = useAppSelector(getAuthoriztionStatus) === AuthorizationStatus.Authorized; return ( isAuthorized ? children @@ -19,7 +20,7 @@ export function PrivateRouteAuthorized(props: PrivateRouteProps): JSX.Element { export function PrivateRouteUnauthorized(props: PrivateRouteProps): JSX.Element { const {children} = props; - const isAuthorized = useAppSelector((state) => state.authorizationStatus) === AuthorizationStatus.Unauthorized; + const isAuthorized = useAppSelector(getAuthoriztionStatus) === AuthorizationStatus.Unauthorized; return ( isAuthorized ? children diff --git a/src/components/review-form/review-form.tsx b/src/components/review-form/review-form.tsx index 52d3a62..d5b8fcf 100644 --- a/src/components/review-form/review-form.tsx +++ b/src/components/review-form/review-form.tsx @@ -1,20 +1,20 @@ -import React, {JSX} from 'react'; +import React, {JSX, memo} from 'react'; import {useState} from 'react'; import {minCommentLength, maxCommentLength} from '../../const.ts'; -import {store} from '../../store'; import {sendReview} from '../../store/api-actions.ts'; -import {useAppSelector} from '../../hooks'; +import {useAppDispatch, useAppSelector} from '../../hooks'; +import {getDetailOffer} from '../../store/detail-offer-data/selectors.ts'; -export default function ReviewForm(): JSX.Element { +function ReviewForm(): JSX.Element { const [formData, setFormData] = useState({ review: '', rating: 0 }); - - const offerId = useAppSelector((state) => state.detailOffer)!.id; + const dispatch = useAppDispatch(); + const offerId = useAppSelector(getDetailOffer)!.id; const handleSubmit: React.FormEventHandler = (evt) => { evt.preventDefault(); - store.dispatch( + dispatch( sendReview({ offerId, rating: formData.rating, @@ -84,3 +84,6 @@ export default function ReviewForm(): JSX.Element { ); } + +const MemoizedReviewForm = memo(ReviewForm); +export default MemoizedReviewForm; diff --git a/src/components/review-item/review-item.tsx b/src/components/review-item/review-item.tsx index 1b955f3..fb0132b 100644 --- a/src/components/review-item/review-item.tsx +++ b/src/components/review-item/review-item.tsx @@ -1,11 +1,11 @@ -import {JSX} from 'react'; +import {JSX, memo} from 'react'; import {Review} from '../../types/review.ts'; type ReviewItemProps = { review: Review; } -export function ReviewItem({review} : ReviewItemProps) : JSX.Element { +function ReviewItem({review} : ReviewItemProps) : JSX.Element { return (
  • @@ -33,3 +33,6 @@ export function ReviewItem({review} : ReviewItemProps) : JSX.Element {
  • ); } + +const MemoizedReviewItem = memo(ReviewItem); +export default MemoizedReviewItem; diff --git a/src/components/review-list/review-list.tsx b/src/components/review-list/review-list.tsx index 00d143b..42ad00a 100644 --- a/src/components/review-list/review-list.tsx +++ b/src/components/review-list/review-list.tsx @@ -1,19 +1,24 @@ -import {JSX} from 'react'; +import {JSX, memo} from 'react'; import {Review} from '../../types/review.ts'; -import {ReviewItem} from '../review-item/review-item.tsx'; +import MemoizedReviewItem from '../review-item/review-item.tsx'; type ReviewListProps = { reviews: Review[]; } -export function ReviewList({reviews} : ReviewListProps) : JSX.Element { +function ReviewList({reviews} : ReviewListProps) : JSX.Element { return (
      {reviews.map((review) => ( - ))}
    ); } + +const MemoizedReviewList = memo(ReviewList, (prevProps, nextProps) => { + return prevProps.reviews === nextProps.reviews; +}); +export default MemoizedReviewList; diff --git a/src/components/sorting/sorting.tsx b/src/components/sorting/sorting.tsx index c4a33e2..1674ddf 100644 --- a/src/components/sorting/sorting.tsx +++ b/src/components/sorting/sorting.tsx @@ -1,5 +1,5 @@ -import {JSX, useState} from 'react'; -import {sortOptions} from '../../const.ts'; +import {JSX, useCallback, useState} from 'react'; +import {SortOptions} from '../../const.ts'; import {SortOption} from '../../types/sort-option.ts'; type SortingProps = { @@ -10,15 +10,15 @@ export function Sorting({onSortChange} : SortingProps) : JSX.Element { const [activeSortOption, setActiveSortOption] = useState('Popular'); const [isOptionsVisible, setIsOptionsVisible] = useState(false); - const handleOptionClick = (option: SortOption) => { + const handleOptionClick = useCallback((option: SortOption) => { setActiveSortOption(option); onSortChange(option); setIsOptionsVisible(false); - }; + }, [onSortChange]); - const handleListOptionClick = () => { - setIsOptionsVisible(!isOptionsVisible); - }; + const handleListOptionClick = useCallback(() => { + setIsOptionsVisible((prevState) => !prevState); + }, []); return (
    @@ -30,7 +30,7 @@ export function Sorting({onSortChange} : SortingProps) : JSX.Element {
      - {sortOptions.map((option) => ( + {SortOptions.map((option) => (
    • ); } + diff --git a/src/const.ts b/src/const.ts index 275d564..6a410b7 100644 --- a/src/const.ts +++ b/src/const.ts @@ -99,17 +99,31 @@ export const Cities : City[] = [ Dusseldorf ]; -export const sortOptions : SortOption[] = [ +export const SortOptions : SortOption[] = [ 'Popular', 'Price: low to high', 'Price: high to low', 'Top rated first', ]; -export const apiRoute = { +export const ApiRoute = { offers: '/offers', favorite: '/favorite', login: '/login', logout: '/logout', reviews: '/comments' }; + +export enum Namespace { + App = 'APP', + Offers = 'OFFERS', + DetailOffer = 'DETAIL_OFFER', + User = 'USER' +} + +export enum FavoriteStatus { + Remove, + Add +} + + diff --git a/src/index.tsx b/src/index.tsx index 5cfac27..e21656c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,14 +3,15 @@ import ReactDOM from 'react-dom/client'; import { App } from './components/app/app.tsx'; import {Provider} from 'react-redux'; import {store} from './store'; -import {checkAuthorization, fetchOffers} from './store/api-actions.ts'; +import {checkAuthorization, fetchFavoritesOffers, fetchOffers} from './store/api-actions.ts'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); -store.dispatch(fetchOffers()); store.dispatch(checkAuthorization()); +store.dispatch(fetchOffers()); +store.dispatch(fetchFavoritesOffers()); root.render( diff --git a/src/pages/favorites-screen/favorite-screen.tsx b/src/pages/favorites-screen/favorite-screen.tsx index ce9ed0a..d95bea1 100644 --- a/src/pages/favorites-screen/favorite-screen.tsx +++ b/src/pages/favorites-screen/favorite-screen.tsx @@ -3,46 +3,20 @@ import {Helmet} from 'react-helmet-async'; import {Link} from 'react-router-dom'; import {AppRoute} from '../../const.ts'; import {useAppSelector} from '../../hooks'; +import MemoizedHeader from '../../components/header/header.tsx'; +import {getFavoritesOffers} from '../../store/offers-data/selectors.ts'; export function FavoriteScreen() : JSX.Element { - const offers = useAppSelector((state) => state.offers); - const favorites = offers.filter((offer) => offer.isFavorite); + const favorites = useAppSelector(getFavoritesOffers); const cities = Array.from(new Set(favorites.map((offer) => offer.city.name))).sort(); return (
      - 6 sities: favorites + 6 cities: favorites -
      - -
      +
      diff --git a/src/pages/login-screen/login-screen.tsx b/src/pages/login-screen/login-screen.tsx index 08b4a8d..500f4a9 100644 --- a/src/pages/login-screen/login-screen.tsx +++ b/src/pages/login-screen/login-screen.tsx @@ -1,38 +1,27 @@ import {FormEvent, JSX, useState} from 'react'; import {Helmet} from 'react-helmet-async'; -import {Link} from 'react-router-dom'; -import {AppRoute} from '../../const.ts'; import {LoginInfo} from '../../types/user.ts'; -import {store} from '../../store'; import {login} from '../../store/api-actions.ts'; +import {useAppDispatch} from '../../hooks'; +import MemoizedHeader from '../../components/header/header.tsx'; export function LoginScreen() : JSX.Element { const [loginInfo, setLoginInfo] = useState({ email: '', password: '' }); - + const dispatch = useAppDispatch(); const submitHandle = (evt: FormEvent) => { evt.preventDefault(); - store.dispatch(login(loginInfo)); + dispatch(login(loginInfo)); }; return (
      - 6 sities: authorization + 6 cities: authorization -
      -
      -
      -
      - - 6 cities logo - -
      -
      -
      -
      +
      diff --git a/src/pages/main-screen/main-screen.tsx b/src/pages/main-screen/main-screen.tsx index ba54a68..b2ebdc3 100644 --- a/src/pages/main-screen/main-screen.tsx +++ b/src/pages/main-screen/main-screen.tsx @@ -1,23 +1,21 @@ import {JSX, useState} from 'react'; import {Helmet} from 'react-helmet-async'; import {OffersList} from '../../components/offers-list/offers-list.tsx'; -import {Link} from 'react-router-dom'; -import {AppRoute, AuthorizationStatus} from '../../const.ts'; import {Map} from '../../components/map/map.tsx'; -import {CitiesList} from '../../components/cities-list/cities-list.tsx'; +import MemoizedCitiesList from '../../components/cities-list/cities-list.tsx'; import {useAppSelector} from '../../hooks'; import {Sorting} from '../../components/sorting/sorting.tsx'; import {SortOption} from '../../types/sort-option.ts'; -import {store} from '../../store'; -import {logout} from '../../store/api-actions.ts'; +import MemoizedHeader from '../../components/header/header.tsx'; +import {getActiveCity} from '../../store/app-data/selectors.ts'; +import {getOffers} from '../../store/offers-data/selectors.ts'; export function MainScreen(): JSX.Element { - const activeCity = useAppSelector((state) => state.activeCity); - const offers = useAppSelector((state) => state.offers).filter( + const activeCity = useAppSelector(getActiveCity); + const offers = useAppSelector(getOffers).filter( (offer) => offer.city.name === activeCity.name, ); - const favoritesCount = offers.filter((offer) => offer.isFavorite).length; const [activeOfferId, setActiveOfferId] = useState(null); const selectedOffer = offers.find((offer) => offer.id === activeOfferId); const placesFoundCaption = offers.length === 0 @@ -40,66 +38,16 @@ export function MainScreen(): JSX.Element { setSortingOption(option); }; - const isAuth = useAppSelector((state) => state.authorizationStatus) === AuthorizationStatus.Authorized; - - const logoutHandle = () => { - store.dispatch(logout()); - }; - return (
      - 6 sities + 6 cities -
      -
      -
      -
      - - 6 cities logo - -
      - {isAuth ? ( - - ) : ( - - )} -
      -
      -
      - +

      Cities

      - +
      diff --git a/src/pages/not-found-screen/not-found-screen.tsx b/src/pages/not-found-screen/not-found-screen.tsx index 78bebed..f9cb755 100644 --- a/src/pages/not-found-screen/not-found-screen.tsx +++ b/src/pages/not-found-screen/not-found-screen.tsx @@ -8,7 +8,7 @@ export function NotFoundScreen() : JSX.Element { return (
      - 6 sities: Page Not Found + 6 cities: Page Not Found

      404

      diff --git a/src/pages/offer-screen/offer-screen.tsx b/src/pages/offer-screen/offer-screen.tsx index 449bee7..7497166 100644 --- a/src/pages/offer-screen/offer-screen.tsx +++ b/src/pages/offer-screen/offer-screen.tsx @@ -1,37 +1,34 @@ -import {JSX, useEffect} from 'react'; +import {JSX, useEffect, useMemo} from 'react'; import {Helmet} from 'react-helmet-async'; -import {Link, Navigate, useParams} from 'react-router-dom'; -import {AppRoute, AuthorizationStatus} from '../../const.ts'; -import ReviewForm from '../../components/review-form/review-form.tsx'; +import {Navigate, useParams} from 'react-router-dom'; +import {AuthorizationStatus} from '../../const.ts'; import {Map} from '../../components/map/map.tsx'; -import {ReviewList} from '../../components/review-list/review-list.tsx'; +import MemoizedReviewList from '../../components/review-list/review-list.tsx'; import {OffersList} from '../../components/offers-list/offers-list.tsx'; -import {useAppSelector} from '../../hooks'; -import {store} from '../../store'; -import {setDetailOffer} from '../../store/action.ts'; -import {fetchDetailOffer, fetchNearOffers, fetchReviews, logout} from '../../store/api-actions.ts'; +import {useAppDispatch, useAppSelector} from '../../hooks'; +import {fetchDetailOffer, fetchNearOffers, fetchReviews} from '../../store/api-actions.ts'; import {Loading} from '../../components/loading/loading.tsx'; +import MemoizedHeader from '../../components/header/header.tsx'; +import {setDetailOffer} from '../../store/detail-offer-data/detail-offer-data.ts'; +import {getDetailOffer, getNearOffers, getReviews} from '../../store/detail-offer-data/selectors.ts'; +import {getAuthoriztionStatus} from '../../store/user-data/selectors.ts'; +import MemoizedReviewForm from '../../components/review-form/review-form.tsx'; export function OfferScreen() : JSX.Element { const {id} = useParams(); + const dispatch = useAppDispatch(); useEffect(() => { - store.dispatch(setDetailOffer(null)); - store.dispatch(fetchDetailOffer(id!)); - store.dispatch(fetchNearOffers(id!)); - store.dispatch(fetchReviews(id!)); + dispatch(setDetailOffer(null)); + dispatch(fetchDetailOffer(id!)); + dispatch(fetchNearOffers(id!)); + dispatch(fetchReviews(id!)); }, [id]); - const offer = useAppSelector((state) => state.detailOffer); - const nearOffers = useAppSelector((state) => state.nearOffers).slice( - 0, - 3, - ); - const reviews = useAppSelector((state) => state.reviews); - const favoriteCount = useAppSelector((state) => state.offers).filter((commonOffer) => commonOffer.isFavorite).length; - const isAuth = useAppSelector((state) => state.authorizationStatus) === AuthorizationStatus.Authorized; - const logoutHandle = () => { - store.dispatch(logout()); - }; + const offer = useAppSelector(getDetailOffer); + const nearOffers = useAppSelector(getNearOffers); + const memoizedNearOffers = useMemo(() => nearOffers.slice(0, 3), [nearOffers]); + const reviews = useAppSelector(getReviews); + const isAuth = useAppSelector(getAuthoriztionStatus) === AuthorizationStatus.Authorized; if (offer === null){ return (); @@ -44,52 +41,9 @@ export function OfferScreen() : JSX.Element { return (
      - 6 sities: {offer.title} + 6 cities: {offer.title} -
      -
      -
      -
      - - 6 cities logo - -
      - {isAuth ? ( - - ) : ( - - )} -
      -
      -
      +
      @@ -176,8 +130,8 @@ export function OfferScreen() : JSX.Element {

      Reviews · {reviews.length}

      - - {isAuth && } + + {isAuth && }
      @@ -191,7 +145,7 @@ export function OfferScreen() : JSX.Element {

      Other places in the neighbourhood

      - {}} /> + {}} />
      diff --git a/src/servises/api.ts b/src/servises/api.ts index a717b24..68be476 100644 --- a/src/servises/api.ts +++ b/src/servises/api.ts @@ -1,8 +1,9 @@ import axios, {AxiosError, AxiosInstance, InternalAxiosRequestConfig} from 'axios'; import {getToken} from './token.ts'; import {store} from '../store'; -import {setAuthorizationStatus, setDetailOffer} from '../store/action.ts'; import {AuthorizationStatus} from '../const.ts'; +import {setAuthorizationStatus} from '../store/user-data/user-data.ts'; +import {setDetailOffer} from '../store/detail-offer-data/detail-offer-data.ts'; const baseURL = 'https://14.design.htmlacademy.pro/six-cities'; const requestTimeout = 5000; @@ -39,7 +40,7 @@ export const createAPI = () : AxiosInstance => { ); } if (error.response && error.response.status === 404) { - store.dispatch(setDetailOffer(undefined)); + store.dispatch(setDetailOffer('NOT_FOUND')); } throw error; }, diff --git a/src/store/action.ts b/src/store/action.ts deleted file mode 100644 index 352a3af..0000000 --- a/src/store/action.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {createAction} from '@reduxjs/toolkit'; -import {City} from '../types/city.ts'; -import {Offer} from '../types/offer.ts'; -import {DetailOffer} from '../types/detail-offer.ts'; -import {Review} from '../types/review.ts'; -import {AuthorizationStatus} from '../const.ts'; -import {Nullable} from 'vitest'; - -export const changeActiveCity = createAction('offers/changeActiveCity'); - -export const setOffers = createAction('offers/setOffers'); - -export const setDetailOffer = createAction>('offers/setDetailOffer'); - -export const setNearOffers = createAction('offers/setNearOffers'); - -export const setReviews = createAction('offers/setReviews'); - -export const setAuthorizationStatus = createAction('auth/setAuthorizationStatus'); diff --git a/src/store/api-actions.ts b/src/store/api-actions.ts index b6d38de..746da9b 100644 --- a/src/store/api-actions.ts +++ b/src/store/api-actions.ts @@ -2,13 +2,16 @@ import {createAsyncThunk} from '@reduxjs/toolkit'; import {AppDispatch, State} from '../types/state.ts'; import {AxiosInstance} from 'axios'; import {Offer} from '../types/offer.ts'; -import {apiRoute, AuthorizationStatus} from '../const.ts'; -import {setAuthorizationStatus, setDetailOffer, setNearOffers, setOffers, setReviews} from './action.ts'; +import {ApiRoute, AuthorizationStatus} from '../const.ts'; import {DetailOffer} from '../types/detail-offer.ts'; import {Review, ReviewInfo} from '../types/review.ts'; -import {saveToken, dropToken} from '../servises/token.ts'; +import {saveToken, dropToken, getToken} from '../servises/token.ts'; import {AuthInfo, LoginInfo} from '../types/user.ts'; import {store} from './index.ts'; +import {FavoriteInfo} from '../types/favorite-info.ts'; +import {setFavoritesCount, setFavoritesOffers, setOffers, updateOffers} from './offers-data/offers-data.ts'; +import {setDetailOffer, setNearOffers, setReviews} from './detail-offer-data/detail-offer-data.ts'; +import {saveUserEmail, setAuthorizationStatus} from './user-data/user-data.ts'; export const fetchOffers = createAsyncThunk( 'data/fetchOffers', async (_arg, {dispatch, extra: api}) => { - const {data} = await api.get(apiRoute.offers); + const {data} = await api.get(ApiRoute.offers); dispatch(setOffers(data)); } ); +export const fetchFavoritesOffers = createAsyncThunk( + 'data/fetchFavoritesOffers', + async (_arg, {dispatch, extra: api}) => { + const {data} = await api.get(ApiRoute.favorite); + dispatch(setFavoritesOffers(data)); + dispatch(setFavoritesCount(data.length)); + } +); + +export const changeFavoriteStatus = createAsyncThunk( + 'data/fetchReviews', + async (favoriteInfo, {dispatch, extra: api}) => { + const {data} = await api.post(`${ApiRoute.favorite}/${favoriteInfo.offerId}/${favoriteInfo.status}`); + dispatch(updateOffers(data)); + dispatch(fetchFavoritesOffers()); + } +); + + export const fetchDetailOffer = createAsyncThunk( 'data/fetchDetailOffer', async (offerId, {dispatch, extra: api}) => { - const {data} = await api.get(`${apiRoute.offers}/${offerId}`); + const {data} = await api.get(`${ApiRoute.offers}/${offerId}`); dispatch(setDetailOffer(data)); } ); @@ -41,7 +71,7 @@ export const fetchNearOffers = createAsyncThunk( 'data/fetchNearOffers', async (offerId, {dispatch, extra: api}) => { - const {data} = await api.get(`${apiRoute.offers}/${offerId}/nearby`); + const {data} = await api.get(`${ApiRoute.offers}/${offerId}/nearby`); dispatch(setNearOffers(data)); } ); @@ -53,7 +83,7 @@ export const fetchReviews = createAsyncThunk( 'data/fetchReviews', async (offerId, {dispatch, extra: api}) => { - const {data} = await api.get(`${apiRoute.reviews}/${offerId}`); + const {data} = await api.get(`${ApiRoute.reviews}/${offerId}`); dispatch(setReviews(data)); } ); @@ -64,7 +94,7 @@ export const sendReview = createAsyncThunk( 'data/fetchReviews', async (reviewInfo, {extra: api}) => { - const response = await api.post(`${apiRoute.reviews}/${reviewInfo.offerId}`, { + const response = await api.post(`${ApiRoute.reviews}/${reviewInfo.offerId}`, { comment: reviewInfo.comment, rating: reviewInfo.rating }); @@ -80,10 +110,12 @@ export const login = createAsyncThunk( 'auth/login', async (loginInfo, { dispatch, extra: api }) => { - const response = await api.post(apiRoute.login, loginInfo); + const response = await api.post(ApiRoute.login, loginInfo); if (response.status === 200 || response.status === 201) { dispatch(setAuthorizationStatus(AuthorizationStatus.Authorized)); saveToken(response.data.token); + dispatch(saveUserEmail(loginInfo.email)); + dispatch(fetchFavoritesOffers()); } else { throw response; } @@ -95,10 +127,16 @@ export const checkAuthorization = createAsyncThunk( 'auth/checkAuthorization', async (_arg, { dispatch, extra: api }) => { - const responce = await api.get(apiRoute.login); - if (responce.status === 200 || responce.status === 201){ - dispatch(setAuthorizationStatus(AuthorizationStatus.Authorized)); - }else { + const token = getToken(); + if (token) { + try{ + await api.get(ApiRoute.login); + dispatch(setAuthorizationStatus(AuthorizationStatus.Authorized)); + dispatch(fetchFavoritesOffers); + } catch { + dispatch(setAuthorizationStatus(AuthorizationStatus.Unauthorized)); + } + } else { dispatch(setAuthorizationStatus(AuthorizationStatus.Unauthorized)); } }); @@ -109,7 +147,7 @@ export const logout = createAsyncThunk( 'auth/logout', async (_arg, { dispatch, extra: api }) => { - await api.delete(apiRoute.logout); + await api.delete(ApiRoute.logout); dropToken(); dispatch(setAuthorizationStatus(AuthorizationStatus.Unauthorized)); }); diff --git a/src/store/app-data/app-data.ts b/src/store/app-data/app-data.ts new file mode 100644 index 0000000..944e1f4 --- /dev/null +++ b/src/store/app-data/app-data.ts @@ -0,0 +1,20 @@ +import {AppData} from '../../types/state.ts'; +import {Namespace, Paris} from '../../const.ts'; +import {createSlice, PayloadAction} from '@reduxjs/toolkit'; +import {City} from '../../types/city.ts'; + +const initialState: AppData = { + city: Paris +}; + +export const appData = createSlice({ + name: Namespace.App, + initialState, + reducers: { + changeActiveCity: (state, action: PayloadAction) => { + state.city = action.payload; + } + } +}); + +export const {changeActiveCity} = appData.actions; diff --git a/src/store/app-data/selectors.ts b/src/store/app-data/selectors.ts new file mode 100644 index 0000000..4abfb58 --- /dev/null +++ b/src/store/app-data/selectors.ts @@ -0,0 +1,5 @@ +import {State} from '../../types/state.ts'; +import {Namespace} from '../../const.ts'; +import {City} from '../../types/city.ts'; + +export const getActiveCity = (state: State): City => state[Namespace.App].city; diff --git a/src/store/detail-offer-data/detail-offer-data.ts b/src/store/detail-offer-data/detail-offer-data.ts new file mode 100644 index 0000000..a76e9ae --- /dev/null +++ b/src/store/detail-offer-data/detail-offer-data.ts @@ -0,0 +1,33 @@ +import {DetailOfferData} from '../../types/state.ts'; +import {createSlice, PayloadAction} from '@reduxjs/toolkit'; +import {Namespace} from '../../const.ts'; +import {DetailOffer} from '../../types/detail-offer.ts'; +import {Offer} from '../../types/offer.ts'; +import {Review} from '../../types/review.ts'; + +const initialState: DetailOfferData = { + detailOffer: null, + nearOffers: [], + reviews: [] +}; + +export const detailOfferData = createSlice({ + name: Namespace.DetailOffer, + initialState, + reducers:{ + setDetailOffer: (state, action: PayloadAction) => { + state.detailOffer = action.payload; + }, + setNearOffers: (state, action: PayloadAction) => { + state.nearOffers = action.payload; + }, + setReviews: (state, action: PayloadAction) => { + state.reviews = action.payload; + }, + sendReview: (state, action: PayloadAction) => { + state.reviews.push(action.payload); + } + } +}); + +export const {setDetailOffer, setNearOffers, setReviews, sendReview} = detailOfferData.actions; diff --git a/src/store/detail-offer-data/selectors.ts b/src/store/detail-offer-data/selectors.ts new file mode 100644 index 0000000..1f1cd4e --- /dev/null +++ b/src/store/detail-offer-data/selectors.ts @@ -0,0 +1,9 @@ +import {State} from '../../types/state.ts'; +import {DetailOffer} from '../../types/detail-offer.ts'; +import {Namespace} from '../../const.ts'; +import {Offer} from '../../types/offer.ts'; +import {Review} from '../../types/review.ts'; + +export const getDetailOffer = (state: State): DetailOffer | null => state[Namespace.DetailOffer].detailOffer; +export const getNearOffers = (state: State): Offer[] => state[Namespace.DetailOffer].nearOffers; +export const getReviews = (state: State): Review[] => state[Namespace.DetailOffer].reviews; diff --git a/src/store/index.ts b/src/store/index.ts index e254bed..c451600 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,11 +1,11 @@ import {configureStore} from '@reduxjs/toolkit'; -import {reducer} from './reducer.ts'; import {createAPI} from '../servises/api.ts'; +import {rootReducer} from './root-reducer.ts'; export const api = createAPI(); export const store = configureStore({ - reducer, + reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: { diff --git a/src/store/offers-data/offers-data.ts b/src/store/offers-data/offers-data.ts new file mode 100644 index 0000000..ce3f7cc --- /dev/null +++ b/src/store/offers-data/offers-data.ts @@ -0,0 +1,33 @@ +import {OffersData} from '../../types/state.ts'; +import {createSlice, PayloadAction} from '@reduxjs/toolkit'; +import {Namespace} from '../../const.ts'; +import {Offer} from '../../types/offer.ts'; + +const initialState: OffersData = { + offers: [], + favoritesOffers: [], + favoritesCount: 0 +}; + +export const offersData = createSlice({ + name: Namespace.Offers, + initialState, + reducers: { + setOffers: (state, action: PayloadAction) => { + state.offers = action.payload; + }, + updateOffers: (state, action: PayloadAction) => { + state.offers = state.offers.map((offer) => + offer.id === action.payload.id ? action.payload : offer + ); + }, + setFavoritesOffers: (state, action: PayloadAction) => { + state.favoritesOffers = action.payload; + }, + setFavoritesCount: (state, action: PayloadAction) => { + state.favoritesCount = action.payload; + } + } +}); + +export const {setOffers, updateOffers, setFavoritesOffers, setFavoritesCount} = offersData.actions; diff --git a/src/store/offers-data/selectors.ts b/src/store/offers-data/selectors.ts new file mode 100644 index 0000000..25decb3 --- /dev/null +++ b/src/store/offers-data/selectors.ts @@ -0,0 +1,9 @@ +import {State} from '../../types/state.ts'; +import {Offer} from '../../types/offer.ts'; +import {Namespace} from '../../const.ts'; + +export const getOffers = (state: State): Offer[] => state[Namespace.Offers].offers; + +export const getFavoritesOffers = (state: State): Offer[] => state[Namespace.Offers].favoritesOffers; + +export const getFavoritesCount = (state: State): number => state[Namespace.Offers].favoritesCount; diff --git a/src/store/reducer.ts b/src/store/reducer.ts deleted file mode 100644 index d978964..0000000 --- a/src/store/reducer.ts +++ /dev/null @@ -1,55 +0,0 @@ -import {AuthorizationStatus, Paris} from '../const.ts'; -import {createReducer} from '@reduxjs/toolkit'; -import { - changeActiveCity, - setAuthorizationStatus, - setDetailOffer, - setNearOffers, - setOffers, - setReviews -} from './action.ts'; -import {City} from '../types/city.ts'; -import {Offer} from '../types/offer.ts'; -import {DetailOffer} from '../types/detail-offer.ts'; -import {Review} from '../types/review.ts'; -import {Nullable} from 'vitest'; - -type InitialState = { - activeCity: City; - offers: Offer[]; - detailOffer: Nullable; - nearOffers: Offer[]; - reviews: Review[]; - authorizationStatus: AuthorizationStatus; -} - -const initialState : InitialState = { - activeCity: Paris, - offers: [], - detailOffer: null, - nearOffers: [], - reviews: [], - authorizationStatus: AuthorizationStatus.Unknown, -}; - -export const reducer = createReducer(initialState, (builder) => { - builder - .addCase(changeActiveCity, (state, action) => { - state.activeCity = action.payload; - }) - .addCase(setOffers, (state, action) => { - state.offers = action.payload; - }) - .addCase(setDetailOffer, (state, action) => { - state.detailOffer = action.payload; - }) - .addCase(setNearOffers, (state, action) => { - state.nearOffers = action.payload; - }) - .addCase(setReviews, (state, action) => { - state.reviews = action.payload; - }) - .addCase(setAuthorizationStatus, (state, action) => { - state.authorizationStatus = action.payload; - }); -}); diff --git a/src/store/root-reducer.ts b/src/store/root-reducer.ts new file mode 100644 index 0000000..15d7cf3 --- /dev/null +++ b/src/store/root-reducer.ts @@ -0,0 +1,13 @@ +import {combineReducers} from '@reduxjs/toolkit'; +import {Namespace} from '../const.ts'; +import {appData} from './app-data/app-data.ts'; +import {detailOfferData} from './detail-offer-data/detail-offer-data.ts'; +import {offersData} from './offers-data/offers-data.ts'; +import {userData} from './user-data/user-data.ts'; + +export const rootReducer = combineReducers({ + [Namespace.App]: appData.reducer, + [Namespace.DetailOffer]: detailOfferData.reducer, + [Namespace.Offers]: offersData.reducer, + [Namespace.User]: userData.reducer +}); diff --git a/src/store/user-data/selectors.ts b/src/store/user-data/selectors.ts new file mode 100644 index 0000000..865be34 --- /dev/null +++ b/src/store/user-data/selectors.ts @@ -0,0 +1,6 @@ +import {State} from '../../types/state.ts'; +import {AuthorizationStatus, Namespace} from '../../const.ts'; + +export const getAuthoriztionStatus = (state: State): AuthorizationStatus => state[Namespace.User].authorizationStatus; + +export const getUserEmail = (state: State): string | null => state[Namespace.User].userEmail; diff --git a/src/store/user-data/user-data.ts b/src/store/user-data/user-data.ts new file mode 100644 index 0000000..da7040a --- /dev/null +++ b/src/store/user-data/user-data.ts @@ -0,0 +1,23 @@ +import {UserData} from '../../types/state.ts'; +import {AuthorizationStatus, Namespace} from '../../const.ts'; +import {createSlice, PayloadAction} from '@reduxjs/toolkit'; + +const initialState: UserData = { + authorizationStatus: AuthorizationStatus.Unknown, + userEmail: null +}; + +export const userData = createSlice({ + name: Namespace.User, + initialState, + reducers: { + setAuthorizationStatus: (state, action: PayloadAction) => { + state.authorizationStatus = action.payload; + }, + saveUserEmail: (state, action: PayloadAction) => { + state.userEmail = action.payload; + } + } +}); + +export const {setAuthorizationStatus, saveUserEmail} = userData.actions; diff --git a/src/types/favorite-info.ts b/src/types/favorite-info.ts new file mode 100644 index 0000000..bd08128 --- /dev/null +++ b/src/types/favorite-info.ts @@ -0,0 +1,6 @@ +import {FavoriteStatus} from '../const.ts'; + +export type FavoriteInfo = { + offerId: string; + status: FavoriteStatus; +}; diff --git a/src/types/state.ts b/src/types/state.ts index aa2ac2c..a5d1c0c 100644 --- a/src/types/state.ts +++ b/src/types/state.ts @@ -1,5 +1,31 @@ import {store} from '../store'; +import {City} from './city.ts'; +import {DetailOffer} from './detail-offer.ts'; +import {Offer} from './offer.ts'; +import {Review} from './review.ts'; +import {AuthorizationStatus} from '../const.ts'; export type State = ReturnType; export type AppDispatch = typeof store.dispatch; + +export type AppData = { + city: City; +}; + +export type DetailOfferData = { + detailOffer: DetailOffer | null | 'NOT_FOUND'; + nearOffers: Offer[]; + reviews: Review[]; +}; + +export type OffersData = { + offers: Offer[]; + favoritesOffers: Offer[]; + favoritesCount: number; +}; + +export type UserData = { + authorizationStatus: AuthorizationStatus; + userEmail: string | null; +} From 8273a2ffe2e01c496f20b1a3af602ec65cece01d Mon Sep 17 00:00:00 2001 From: ktvtk Date: Sun, 1 Dec 2024 20:02:35 +0500 Subject: [PATCH 2/2] hw 8.1 --- src/components/place-card/place-card.tsx | 6 ++---- src/components/review-list/review-list.tsx | 4 +--- src/pages/offer-screen/offer-screen.tsx | 2 +- src/servises/api.ts | 2 +- src/store/detail-offer-data/detail-offer-data.ts | 2 +- src/store/detail-offer-data/selectors.ts | 2 +- src/types/state.ts | 2 +- 7 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/components/place-card/place-card.tsx b/src/components/place-card/place-card.tsx index 5870f8f..246f495 100644 --- a/src/components/place-card/place-card.tsx +++ b/src/components/place-card/place-card.tsx @@ -28,7 +28,7 @@ function PlaceCard({offer, onMouseLeave, onMouseEnter}: PlaceCardProps): JSX.Ele } else { navigate(AppRoute.Login); } - }, [isAuth, isFavorite, offer, dispatch, id]); + }, [isAuth, isFavorite, offer, dispatch, id, navigate]); return (
      { - return prevProps.offer.id === nextProps.offer.id && prevProps.offer.isFavorite === nextProps.offer.isFavorite; -}); +const MemoizedPlaceCard = memo(PlaceCard, (prevProps, nextProps) => prevProps.offer.id === nextProps.offer.id && prevProps.offer.isFavorite === nextProps.offer.isFavorite); export default MemoizedPlaceCard; diff --git a/src/components/review-list/review-list.tsx b/src/components/review-list/review-list.tsx index 42ad00a..8e70d09 100644 --- a/src/components/review-list/review-list.tsx +++ b/src/components/review-list/review-list.tsx @@ -18,7 +18,5 @@ function ReviewList({reviews} : ReviewListProps) : JSX.Element { ); } -const MemoizedReviewList = memo(ReviewList, (prevProps, nextProps) => { - return prevProps.reviews === nextProps.reviews; -}); +const MemoizedReviewList = memo(ReviewList, (prevProps, nextProps) => prevProps.reviews === nextProps.reviews); export default MemoizedReviewList; diff --git a/src/pages/offer-screen/offer-screen.tsx b/src/pages/offer-screen/offer-screen.tsx index 7497166..2fcab2a 100644 --- a/src/pages/offer-screen/offer-screen.tsx +++ b/src/pages/offer-screen/offer-screen.tsx @@ -22,7 +22,7 @@ export function OfferScreen() : JSX.Element { dispatch(fetchDetailOffer(id!)); dispatch(fetchNearOffers(id!)); dispatch(fetchReviews(id!)); - }, [id]); + }, [dispatch, id]); const offer = useAppSelector(getDetailOffer); const nearOffers = useAppSelector(getNearOffers); diff --git a/src/servises/api.ts b/src/servises/api.ts index 68be476..ae718db 100644 --- a/src/servises/api.ts +++ b/src/servises/api.ts @@ -40,7 +40,7 @@ export const createAPI = () : AxiosInstance => { ); } if (error.response && error.response.status === 404) { - store.dispatch(setDetailOffer('NOT_FOUND')); + store.dispatch(setDetailOffer(undefined)); } throw error; }, diff --git a/src/store/detail-offer-data/detail-offer-data.ts b/src/store/detail-offer-data/detail-offer-data.ts index a76e9ae..820f293 100644 --- a/src/store/detail-offer-data/detail-offer-data.ts +++ b/src/store/detail-offer-data/detail-offer-data.ts @@ -15,7 +15,7 @@ export const detailOfferData = createSlice({ name: Namespace.DetailOffer, initialState, reducers:{ - setDetailOffer: (state, action: PayloadAction) => { + setDetailOffer: (state, action: PayloadAction) => { state.detailOffer = action.payload; }, setNearOffers: (state, action: PayloadAction) => { diff --git a/src/store/detail-offer-data/selectors.ts b/src/store/detail-offer-data/selectors.ts index 1f1cd4e..fe2c0e2 100644 --- a/src/store/detail-offer-data/selectors.ts +++ b/src/store/detail-offer-data/selectors.ts @@ -4,6 +4,6 @@ import {Namespace} from '../../const.ts'; import {Offer} from '../../types/offer.ts'; import {Review} from '../../types/review.ts'; -export const getDetailOffer = (state: State): DetailOffer | null => state[Namespace.DetailOffer].detailOffer; +export const getDetailOffer = (state: State): DetailOffer | null | undefined=> state[Namespace.DetailOffer].detailOffer; export const getNearOffers = (state: State): Offer[] => state[Namespace.DetailOffer].nearOffers; export const getReviews = (state: State): Review[] => state[Namespace.DetailOffer].reviews; diff --git a/src/types/state.ts b/src/types/state.ts index a5d1c0c..e3673f0 100644 --- a/src/types/state.ts +++ b/src/types/state.ts @@ -14,7 +14,7 @@ export type AppData = { }; export type DetailOfferData = { - detailOffer: DetailOffer | null | 'NOT_FOUND'; + detailOffer: DetailOffer | null | undefined; nearOffers: Offer[]; reviews: Review[]; };