From 1b58f437c769678fee5d0624b82542fb090df0b6 Mon Sep 17 00:00:00 2001 From: Gurikov Maxim <maximgurikoff@gmail.com> Date: Sun, 8 Dec 2024 17:46:35 +0500 Subject: [PATCH 1/5] fixes --- src/Pages/login-page/login-page.tsx | 7 ++--- src/api/api.ts | 26 +++++++++++++++++-- src/components/reviews/reviews-list.tsx | 6 +++-- src/components/reviews/reviews.tsx | 9 ++++--- .../current-offer/current-offer.selectors.ts | 8 +++--- 5 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/Pages/login-page/login-page.tsx b/src/Pages/login-page/login-page.tsx index 321229a..7482064 100644 --- a/src/Pages/login-page/login-page.tsx +++ b/src/Pages/login-page/login-page.tsx @@ -25,7 +25,6 @@ export function LoginPage(): React.JSX.Element { loginInfo.email && validateEmail(loginInfo.email) && loginInfo.password && - loginInfo.password.length > 3 && loginInfo.password.match(/[a-zA-z]/g) && loginInfo.password.match(/[0-9]/g); return ( @@ -47,7 +46,8 @@ export function LoginPage(): React.JSX.Element { name="email" placeholder="Email" onChange={(event) => - setLoginInfo({ ...loginInfo, email: event.target.value })} + setLoginInfo({ ...loginInfo, email: event.target.value }) + } required /> </div> @@ -62,7 +62,8 @@ export function LoginPage(): React.JSX.Element { setLoginInfo({ ...loginInfo, password: event.target.value, - })} + }) + } required /> </div> diff --git a/src/api/api.ts b/src/api/api.ts index 2713287..1ac4a1b 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,8 +1,13 @@ -import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios'; +import axios, { + AxiosError, + AxiosInstance, + InternalAxiosRequestConfig, +} from 'axios'; import { getToken } from '../utils/token-utils.ts'; +import { toast } from 'react-toastify'; const BACKEND_URL = 'https://14.design.htmlacademy.pro/six-cities'; -const REQUEST_TIMEOUT = 5000; +const REQUEST_TIMEOUT = 3000; export const createAPI = (): AxiosInstance => { const api = axios.create({ @@ -20,5 +25,22 @@ export const createAPI = (): AxiosInstance => { return config; }); + api.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + if ( + error && + (error.code === 'ECONNABORTED' || error.code === 'ERR_NETWORK') + ) { + toast.error( + 'Сервер недоступен, проверте подключение к интернету или повторите попытку позже', + { + toastId: 'server-unreachable', + }, + ); + } + }, + ); + return api; }; diff --git a/src/components/reviews/reviews-list.tsx b/src/components/reviews/reviews-list.tsx index 8f463fa..e6c4853 100644 --- a/src/components/reviews/reviews-list.tsx +++ b/src/components/reviews/reviews-list.tsx @@ -1,16 +1,18 @@ import { Review } from '../../dataTypes/review.ts'; import { ReviewComponent } from './review-component.tsx'; +import { useAppSelector } from '../../store/store.ts'; +import { getReviewsCount } from '../../store/current-offer/current-offer.selectors.ts'; interface ReviewsListProps { reviews: Review[]; } export function ReviewsList({ reviews }: ReviewsListProps): React.JSX.Element { + const reviewsCount = useAppSelector(getReviewsCount); return ( <> <h2 className="reviews__title"> - Reviews ·{' '} - <span className="reviews__amount">{reviews.length}</span> + Reviews · <span className="reviews__amount">{reviewsCount}</span> </h2> <ul className="reviews__list"> {reviews.map((review: Review) => ( diff --git a/src/components/reviews/reviews.tsx b/src/components/reviews/reviews.tsx index d613ce3..1bdc9eb 100644 --- a/src/components/reviews/reviews.tsx +++ b/src/components/reviews/reviews.tsx @@ -4,6 +4,7 @@ import { Review } from '../../dataTypes/review.ts'; import { useAppSelector } from '../../store/store.ts'; import { useMemo } from 'react'; import { getIsAuthorized } from '../../store/user/user.selectors.ts'; +import { MAX_REVIEWS_COUNT } from '../../consts/reviews.ts'; interface ReviewsProps { reviews: Review[]; @@ -12,9 +13,11 @@ interface ReviewsProps { export function Reviews({ reviews }: ReviewsProps): React.JSX.Element { const sortedReviews = useMemo( () => - reviews.toSorted( - (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), - ), + reviews + .toSorted( + (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), + ) + .slice(0, MAX_REVIEWS_COUNT), [reviews], ); const isAuthorized = useAppSelector(getIsAuthorized); diff --git a/src/store/current-offer/current-offer.selectors.ts b/src/store/current-offer/current-offer.selectors.ts index 36c7d10..bd74d1e 100644 --- a/src/store/current-offer/current-offer.selectors.ts +++ b/src/store/current-offer/current-offer.selectors.ts @@ -1,7 +1,6 @@ import { State } from '../../dataTypes/store-types.ts'; import { NameSpaces } from '../../dataTypes/enums/name-spaces.ts'; import { MAX_NEARBY_OFFERS } from '../../consts/offers.ts'; -import { MAX_REVIEWS_COUNT } from '../../consts/reviews.ts'; import { createSelector } from '@reduxjs/toolkit'; import { Offer } from '../../dataTypes/offer.ts'; import { Review } from '../../dataTypes/review.ts'; @@ -12,9 +11,12 @@ export const getNearbyOffers = createSelector( [(state: State) => state[NameSpaces.CurrentOffer].nearbyOffers], (offers: Offer[]) => offers.slice(0, MAX_NEARBY_OFFERS), ); -export const getCurrentReviews = createSelector( +export const getCurrentReviews = (state: State) => + state[NameSpaces.CurrentOffer].currentReviews; + +export const getReviewsCount = createSelector( [(state: State) => state[NameSpaces.CurrentOffer].currentReviews], - (reviews: Review[]) => reviews.slice(0, MAX_REVIEWS_COUNT), + (reviews: Review[]) => reviews.length, ); export const getReviewPostingStatus = (state: State) => state[NameSpaces.CurrentOffer].reviewPostingStatus; From 7c12e24c3e7a3f908027a9ad248ed5be095e23bc Mon Sep 17 00:00:00 2001 From: Gurikov Maxim <maximgurikoff@gmail.com> Date: Tue, 10 Dec 2024 23:30:02 +0500 Subject: [PATCH 2/5] added test and naming fixes --- .../login-page/login-page-right-section.tsx | 4 +- src/Pages/not-found-page/not-found-page.tsx | 8 +- src/Pages/offer-page/offer-page.tsx | 4 +- src/components/app.tsx | 14 ++-- src/components/authorization-wrapper.tsx | 4 +- src/components/bookmark-button.tsx | 4 +- src/components/layout/footer.tsx | 4 +- src/components/layout/header.tsx | 6 +- src/components/layout/user-info.tsx | 6 +- src/components/offer/offer-card.tsx | 8 +- src/components/reviews/review-form.tsx | 9 ++- .../enums/{api-routes.ts => api-route.ts} | 2 +- .../enums/{app-routes.ts => app-route.ts} | 2 +- .../enums/{name-spaces.ts => name-space.ts} | 2 +- src/mocks/mock-detailed-offer.ts | 36 +++++++++ src/mocks/mock-offers.ts | 43 ++++++++++ src/mocks/mock-review.ts | 21 +++++ src/mocks/mock-user.ts | 20 +++++ src/setupTests.ts | 2 +- src/store/async-actions.ts | 22 ++--- .../current-offer/current-offer.selectors.ts | 20 ++--- .../current-offer/current-offer.slice.test.ts | 80 +++++++++++++++++++ .../current-offer/current-offer.slice.ts | 6 +- .../current-offers.selectors.test.ts | 48 +++++++++++ src/store/offers/offers.selector.test.ts | 37 +++++++++ src/store/offers/offers.selectors.ts | 16 ++-- src/store/offers/offers.slice.test.ts | 57 +++++++++++++ src/store/offers/offers.slice.ts | 4 +- src/store/store.ts | 8 +- src/store/user/user-slice.ts | 4 +- src/store/user/user.selector.test.ts | 0 src/store/user/user.selectors.ts | 10 ++- src/store/user/user.slice.test.ts | 0 33 files changed, 431 insertions(+), 80 deletions(-) rename src/dataTypes/enums/{api-routes.ts => api-route.ts} (81%) rename src/dataTypes/enums/{app-routes.ts => app-route.ts} (83%) rename src/dataTypes/enums/{name-spaces.ts => name-space.ts} (74%) create mode 100644 src/mocks/mock-detailed-offer.ts create mode 100644 src/mocks/mock-offers.ts create mode 100644 src/mocks/mock-review.ts create mode 100644 src/mocks/mock-user.ts create mode 100644 src/store/current-offer/current-offer.slice.test.ts create mode 100644 src/store/current-offer/current-offers.selectors.test.ts create mode 100644 src/store/offers/offers.selector.test.ts create mode 100644 src/store/offers/offers.slice.test.ts create mode 100644 src/store/user/user.selector.test.ts create mode 100644 src/store/user/user.slice.test.ts diff --git a/src/Pages/login-page/login-page-right-section.tsx b/src/Pages/login-page/login-page-right-section.tsx index 4379bde..a17bcaa 100644 --- a/src/Pages/login-page/login-page-right-section.tsx +++ b/src/Pages/login-page/login-page-right-section.tsx @@ -1,5 +1,5 @@ import { Link } from 'react-router-dom'; -import { AppRoutes } from '../../dataTypes/enums/app-routes.ts'; +import { AppRoute } from '../../dataTypes/enums/app-route.ts'; import { changeCity } from '../../store/offers/offers.slice.ts'; import { CITIES } from '../../consts/cities.ts'; import { useAppDispatch } from '../../store/store.ts'; @@ -13,7 +13,7 @@ function LoginPageRightSectionImpl() { <div className="locations__item"> <Link className="locations__item-link" - to={AppRoutes.MainPage} + to={AppRoute.MainPage} onClick={() => dispatch(changeCity(city))} > <span>{city.name}</span> diff --git a/src/Pages/not-found-page/not-found-page.tsx b/src/Pages/not-found-page/not-found-page.tsx index 05e617a..1e4f606 100644 --- a/src/Pages/not-found-page/not-found-page.tsx +++ b/src/Pages/not-found-page/not-found-page.tsx @@ -1,19 +1,19 @@ import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet-async'; -import {AppRoutes} from '../../dataTypes/enums/app-routes.ts'; +import { AppRoute } from '../../dataTypes/enums/app-route.ts'; export function NotFoundPage(): React.JSX.Element { return ( - <main className='not-found-page'> + <main className="not-found-page"> <Helmet> <title>404 - not found</title> </Helmet> <h1>404 - Page Not Found</h1> - <p className='not-found-page__title'> + <p className="not-found-page__title"> The page you are looking for might have been removed or is temporarily unavailable. </p> - <Link to={AppRoutes.MainPage} className='not-found-page__link'> + <Link to={AppRoute.MainPage} className="not-found-page__link"> back to main page </Link> </main> diff --git a/src/Pages/offer-page/offer-page.tsx b/src/Pages/offer-page/offer-page.tsx index 0b2d93b..b1a6907 100644 --- a/src/Pages/offer-page/offer-page.tsx +++ b/src/Pages/offer-page/offer-page.tsx @@ -13,7 +13,7 @@ import { OfferGallery } from '../../components/offer/offer-gallery.tsx'; import { BookmarkButton } from '../../components/bookmark-button.tsx'; import { store, useAppSelector } from '../../store/store.ts'; import { Spinner } from '../../components/spinner/Spinner.tsx'; -import { AppRoutes } from '../../dataTypes/enums/app-routes.ts'; +import { AppRoute } from '../../dataTypes/enums/app-route.ts'; import { fetchNearbyOffers, fetchOffer, @@ -38,7 +38,7 @@ export function OfferPage(): React.JSX.Element { const currentOffer = useAppSelector(getCurrentOffer); const currentReviews = useAppSelector(getCurrentReviews); if (currentOffer === undefined) { - return <Navigate to={AppRoutes.NotFoundPage} />; + return <Navigate to={AppRoute.NotFoundPage} />; } return ( <div className="page"> diff --git a/src/components/app.tsx b/src/components/app.tsx index f875bc1..cafc1e3 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -8,7 +8,7 @@ import { AuthorizationWrapperForAuthorizedOnly, AuthorizationWrapperForUnauthorizedOnly, } from './authorization-wrapper.tsx'; -import { AppRoutes } from '../dataTypes/enums/app-routes.ts'; +import { AppRoute } from '../dataTypes/enums/app-route.ts'; import { HelmetProvider } from 'react-helmet-async'; import { Provider } from 'react-redux'; import { store } from '../store/store.ts'; @@ -19,28 +19,28 @@ export function App(): React.JSX.Element { <HelmetProvider> <BrowserRouter> <Routes> - <Route path={AppRoutes.MainPage} element={<MainPage />} /> + <Route path={AppRoute.MainPage} element={<MainPage />} /> <Route - path={AppRoutes.Login} + path={AppRoute.Login} element={ <AuthorizationWrapperForUnauthorizedOnly - fallbackUrl={AppRoutes.MainPage} + fallbackUrl={AppRoute.MainPage} > <LoginPage /> </AuthorizationWrapperForUnauthorizedOnly> } /> <Route - path={AppRoutes.Favorites} + path={AppRoute.Favorites} element={ <AuthorizationWrapperForAuthorizedOnly - fallbackUrl={AppRoutes.Login} + fallbackUrl={AppRoute.Login} > <FavoritesPage /> </AuthorizationWrapperForAuthorizedOnly> } /> - <Route path={`${AppRoutes.Offer}/:id`} element={<OfferPage />} /> + <Route path={`${AppRoute.Offer}/:id`} element={<OfferPage />} /> <Route path="*" element={<NotFoundPage />} /> </Routes> </BrowserRouter> diff --git a/src/components/authorization-wrapper.tsx b/src/components/authorization-wrapper.tsx index 02c40aa..7347046 100644 --- a/src/components/authorization-wrapper.tsx +++ b/src/components/authorization-wrapper.tsx @@ -1,11 +1,11 @@ import { Navigate } from 'react-router-dom'; import { useAppSelector } from '../store/store.ts'; -import { AppRoutes } from '../dataTypes/enums/app-routes.ts'; +import { AppRoute } from '../dataTypes/enums/app-route.ts'; import { getIsAuthorized } from '../store/user/user.selectors.ts'; interface AuthorizationWrapperProps { children: React.JSX.Element; - fallbackUrl: AppRoutes; + fallbackUrl: AppRoute; } export function AuthorizationWrapperForAuthorizedOnly({ diff --git a/src/components/bookmark-button.tsx b/src/components/bookmark-button.tsx index 7fb2314..226ddf6 100644 --- a/src/components/bookmark-button.tsx +++ b/src/components/bookmark-button.tsx @@ -4,7 +4,7 @@ import { bookmarkOffer } from '../store/async-actions.ts'; import { Offer } from '../dataTypes/offer.ts'; import { getIsAuthorized } from '../store/user/user.selectors.ts'; import { useNavigate } from 'react-router-dom'; -import { AppRoutes } from '../dataTypes/enums/app-routes.ts'; +import { AppRoute } from '../dataTypes/enums/app-route.ts'; interface BookmarkButtonProps { size: 'big' | 'small'; @@ -45,7 +45,7 @@ export function BookmarkButton({ ); setIsFavoriteReactive(!isFavoriteReactive); } else { - navigate(AppRoutes.Login); + navigate(AppRoute.Login); } }} > diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index dd9d65a..f9b93da 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -1,11 +1,11 @@ import { Link } from 'react-router-dom'; -import { AppRoutes } from '../../dataTypes/enums/app-routes.ts'; +import { AppRoute } from '../../dataTypes/enums/app-route.ts'; import { memo } from 'react'; function FooterImpl() { return ( <footer className="footer container"> - <Link className="footer__logo-link" to={AppRoutes.MainPage}> + <Link className="footer__logo-link" to={AppRoute.MainPage}> <img className="footer__logo" src="img/logo.svg" diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 5c49f9e..a33fc12 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -1,5 +1,5 @@ import { Link } from 'react-router-dom'; -import { AppRoutes } from '../../dataTypes/enums/app-routes.ts'; +import { AppRoute } from '../../dataTypes/enums/app-route.ts'; import { useAppSelector } from '../../store/store.ts'; import { memo } from 'react'; import { UserInfo } from './user-info.tsx'; @@ -16,7 +16,7 @@ function HeaderImpl({ dontShowUserInfo }: HeaderProps) { <div className="container"> <div className="header__wrapper"> <div className="header__left"> - <Link className="header__logo-link" to={AppRoutes.MainPage}> + <Link className="header__logo-link" to={AppRoute.MainPage}> <img className="header__logo" src="img/logo.svg" @@ -35,7 +35,7 @@ function HeaderImpl({ dontShowUserInfo }: HeaderProps) { <li className="header__nav-item user"> <Link className="header__nav-link header__nav-link--profile" - to={AppRoutes.Login} + to={AppRoute.Login} > <div className="header__avatar-wrapper user__avatar-wrapper"></div> <span className="header__login">Sign in</span> diff --git a/src/components/layout/user-info.tsx b/src/components/layout/user-info.tsx index acdacac..f265a76 100644 --- a/src/components/layout/user-info.tsx +++ b/src/components/layout/user-info.tsx @@ -1,5 +1,5 @@ import { Link, useNavigate } from 'react-router-dom'; -import { AppRoutes } from '../../dataTypes/enums/app-routes.ts'; +import { AppRoute } from '../../dataTypes/enums/app-route.ts'; import { useAppDispatch, useAppSelector } from '../../store/store.ts'; import { logout } from '../../store/async-actions.ts'; import { memo } from 'react'; @@ -15,7 +15,7 @@ function UserInfoImpl() { const handleLogout = () => { dispatch(logout()); dispatch(setFavoriteOffers([])); - navigate(AppRoutes.MainPage); + navigate(AppRoute.MainPage); }; return ( <nav className="header__nav"> @@ -23,7 +23,7 @@ function UserInfoImpl() { <li className="header__nav-item user"> <Link className="header__nav-link header__nav-link--profile" - to={AppRoutes.Favorites} + to={AppRoute.Favorites} > <div className="header__avatar-wrapper user__avatar-wrapper"> {userInfo?.avatarUrl && ( diff --git a/src/components/offer/offer-card.tsx b/src/components/offer/offer-card.tsx index f7c9f4f..71c150c 100644 --- a/src/components/offer/offer-card.tsx +++ b/src/components/offer/offer-card.tsx @@ -1,5 +1,5 @@ import { Link } from 'react-router-dom'; -import { AppRoutes } from '../../dataTypes/enums/app-routes.ts'; +import { AppRoute } from '../../dataTypes/enums/app-route.ts'; import cn from 'classnames'; import { Rating } from '../rating.tsx'; import { BookmarkButton } from '../bookmark-button.tsx'; @@ -38,7 +38,7 @@ export function OfferCardImpl({ onMouseLeave={handleMouseLeave} className={cn( 'place-card', - { 'cities__card': isOnMainPage }, + { cities__card: isOnMainPage }, { 'near-places__card': !isOnMainPage }, )} > @@ -54,7 +54,7 @@ export function OfferCardImpl({ { 'near-places__image-wrapper': !isOnMainPage }, )} > - <Link to={`${AppRoutes.Offer}/${id}`}> + <Link to={`${AppRoute.Offer}/${id}`}> <img className="place-card__image" src={previewImage} @@ -79,7 +79,7 @@ export function OfferCardImpl({ </div> <Rating rating={rating} usePlace="place-card" /> <h2 className="place-card__name"> - <Link to={`${AppRoutes.Offer}/${id}`}>{title}</Link> + <Link to={`${AppRoute.Offer}/${id}`}>{title}</Link> </h2> <p className="place-card__type">{capitalize(type)}</p> </div> diff --git a/src/components/reviews/review-form.tsx b/src/components/reviews/review-form.tsx index 9827ad3..4c2df7b 100644 --- a/src/components/reviews/review-form.tsx +++ b/src/components/reviews/review-form.tsx @@ -24,9 +24,13 @@ export function ReviewForm(): React.JSX.Element { const offerId = useAppSelector(getCurrentOffer)!.id; const reviewPostingStatus = useAppSelector(getReviewPostingStatus); useEffect(() => { - if (reviewPostingStatus === ReviewStatus.Success) { + let isMounted = true; + if (isMounted && reviewPostingStatus === ReviewStatus.Success) { setReview({ comment: '', rating: undefined }); } + return () => { + isMounted = false; + }; }, [reviewPostingStatus]); const onRatingChange: React.ChangeEventHandler<HTMLInputElement> = ( event, @@ -166,8 +170,7 @@ export function ReviewForm(): React.JSX.Element { value={review?.comment || ''} onChange={onCommentChange} disabled={reviewPostingStatus === ReviewStatus.Pending} - > - </textarea> + ></textarea> <div className="reviews__button-wrapper"> <p className="reviews__help"> To submit review please make sure to set{' '} diff --git a/src/dataTypes/enums/api-routes.ts b/src/dataTypes/enums/api-route.ts similarity index 81% rename from src/dataTypes/enums/api-routes.ts rename to src/dataTypes/enums/api-route.ts index 4b9b4bc..d6dcb13 100644 --- a/src/dataTypes/enums/api-routes.ts +++ b/src/dataTypes/enums/api-route.ts @@ -1,4 +1,4 @@ -export enum ApiRoutes { +export enum ApiRoute { Offers = '/offers', Login = '/login', Logout = '/logout', diff --git a/src/dataTypes/enums/app-routes.ts b/src/dataTypes/enums/app-route.ts similarity index 83% rename from src/dataTypes/enums/app-routes.ts rename to src/dataTypes/enums/app-route.ts index fafd2cf..d192e16 100644 --- a/src/dataTypes/enums/app-routes.ts +++ b/src/dataTypes/enums/app-route.ts @@ -1,4 +1,4 @@ -export enum AppRoutes { +export enum AppRoute { MainPage = '/', Login = '/login', Offer = '/offer', diff --git a/src/dataTypes/enums/name-spaces.ts b/src/dataTypes/enums/name-space.ts similarity index 74% rename from src/dataTypes/enums/name-spaces.ts rename to src/dataTypes/enums/name-space.ts index 3047c6d..baf8d7e 100644 --- a/src/dataTypes/enums/name-spaces.ts +++ b/src/dataTypes/enums/name-space.ts @@ -1,4 +1,4 @@ -export enum NameSpaces { +export enum NameSpace { Offers = 'Offers', CurrentOffer = 'CurrentOffer', User = 'User', diff --git a/src/mocks/mock-detailed-offer.ts b/src/mocks/mock-detailed-offer.ts new file mode 100644 index 0000000..4fc0963 --- /dev/null +++ b/src/mocks/mock-detailed-offer.ts @@ -0,0 +1,36 @@ +import * as faker from 'faker'; +import { getMockCity, getMockLocation } from './mock-offers.ts'; +import { DetailedOffer } from '../dataTypes/detailed-offer.ts'; +import { RoomType } from '../dataTypes/enums/room-type.ts'; + +export function getMockDetailedOffer(): DetailedOffer { + return { + id: faker.datatype.uuid(), + title: faker.commerce.productName(), + type: RoomType.Hotel, + price: faker.datatype.number({ min: 100, max: 500 }), + city: getMockCity(), + location: getMockLocation(), + isFavorite: faker.datatype.boolean(), + isPremium: faker.datatype.boolean(), + rating: faker.datatype.number({ min: 1, max: 5, precision: 0.1 }), + description: faker.lorem.paragraph(), + bedrooms: faker.datatype.number({ min: 1, max: 5 }), + goods: [ + faker.commerce.product(), + faker.commerce.product(), + faker.commerce.product(), + ], + host: { + name: faker.name.findName(), + avatarUrl: faker.image.avatar(), + isPro: faker.datatype.boolean(), + }, + images: [ + faker.image.imageUrl(), + faker.image.imageUrl(), + faker.image.imageUrl(), + ], + maxAdults: faker.datatype.number({ min: 1, max: 10 }), + }; +} diff --git a/src/mocks/mock-offers.ts b/src/mocks/mock-offers.ts new file mode 100644 index 0000000..16ab7c9 --- /dev/null +++ b/src/mocks/mock-offers.ts @@ -0,0 +1,43 @@ +import * as faker from 'faker'; +import { Location } from '../dataTypes/location.ts'; +import { RoomType } from '../dataTypes/enums/room-type.ts'; +import { City } from '../dataTypes/city.ts'; +import { Offer } from '../dataTypes/offer.ts'; + +export function getMockLocation(): Location { + return { + latitude: Math.random() * 10 + 50, + longitude: Math.random() + 4, + zoom: faker.datatype.number({ min: 8, max: 15 }), + }; +} + +export function getMockCity(): City { + return { + name: 'Amsterdam', + location: getMockLocation(), + }; +} + +export function getMockOffers(count: number): Offer[] { + const list = []; + + for (let i = 0; i < count; i++) { + const offer = { + id: faker.datatype.uuid(), + title: faker.commerce.productName(), + type: RoomType.Apartment, + price: faker.datatype.number({ min: 100, max: 500 }), + city: getMockCity(), + location: getMockLocation(), + isFavorite: faker.datatype.boolean(), + isPremium: faker.datatype.boolean(), + rating: faker.datatype.number({ min: 1, max: 5, precision: 0.1 }), + previewImage: faker.image.imageUrl(), + }; + + list.push(offer); + } + + return list; +} diff --git a/src/mocks/mock-review.ts b/src/mocks/mock-review.ts new file mode 100644 index 0000000..760a72a --- /dev/null +++ b/src/mocks/mock-review.ts @@ -0,0 +1,21 @@ +import * as faker from 'faker'; +import { getMockUser } from './mock-user.ts'; +import { Review } from '../dataTypes/review.ts'; + +export function getMockReview(): Review { + return { + id: faker.datatype.uuid(), + date: faker.date.recent().toDateString(), + user: getMockUser(), + comment: faker.lorem.sentence(), + rating: faker.datatype.number({ min: 1, max: 5 }), + }; +} + +export function getMockReviews(count: number): Review[] { + const reviews = []; + for (let i = 0; i < count; i++) { + reviews.push(getMockReview()); + } + return reviews; +} diff --git a/src/mocks/mock-user.ts b/src/mocks/mock-user.ts new file mode 100644 index 0000000..5c575af --- /dev/null +++ b/src/mocks/mock-user.ts @@ -0,0 +1,20 @@ +import * as faker from 'faker'; +import { AuthInfo, User } from '../dataTypes/user.ts'; + +export function getMockUser(): User { + return { + name: faker.name.findName(), + avatarUrl: faker.image.avatar(), + isPro: faker.datatype.boolean(), + }; +} + +export function getMockAuthInfo(): AuthInfo { + return { + name: faker.name.findName(), + avatarUrl: faker.image.avatar(), + isPro: faker.datatype.boolean(), + email: faker.internet.email(), + token: faker.random.alphaNumeric(16), + }; +} diff --git a/src/setupTests.ts b/src/setupTests.ts index b210af5..43eb80c 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -1,4 +1,4 @@ -import matchers from '@testing-library/jest-dom/matchers'; import { expect } from 'vitest'; +import * as matchers from '@testing-library/jest-dom/matchers'; expect.extend(matchers); diff --git a/src/store/async-actions.ts b/src/store/async-actions.ts index 6070dab..0e1e9f7 100644 --- a/src/store/async-actions.ts +++ b/src/store/async-actions.ts @@ -2,7 +2,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { AppDispatch, State } from '../dataTypes/store-types.ts'; import axios, { AxiosError, AxiosInstance } from 'axios'; import { Offer } from '../dataTypes/offer.ts'; -import { ApiRoutes } from '../dataTypes/enums/api-routes.ts'; +import { ApiRoute } from '../dataTypes/enums/api-route.ts'; import { DetailedOffer } from '../dataTypes/detailed-offer.ts'; import { AuthInfo, LoginInfo } from '../dataTypes/user.ts'; import { AuthorizationStatus } from '../dataTypes/enums/authorization-status.ts'; @@ -29,7 +29,7 @@ export const fetchOffers = createAsyncThunk< extra: AxiosInstance; } >('data/fetchOffers', async (_arg, { dispatch, extra: api }) => { - const { data } = await api.get<Offer[]>(ApiRoutes.Offers); + const { data } = await api.get<Offer[]>(ApiRoute.Offers); dispatch(setOffers(data)); }); @@ -43,7 +43,7 @@ export const fetchOffer = createAsyncThunk< } >('data/fetchOffer', async (id, { dispatch, extra: api }) => { try { - const { data } = await api.get<DetailedOffer>(`${ApiRoutes.Offers}/${id}`); + const { data } = await api.get<DetailedOffer>(`${ApiRoute.Offers}/${id}`); dispatch(setCurrentOffer(data)); } catch (err) { const error = err as Error | AxiosError; @@ -66,7 +66,7 @@ export const fetchNearbyOffers = createAsyncThunk< extra: AxiosInstance; } >('data/fetchNearbyOffers', async (id, { dispatch, extra: api }) => { - const { data } = await api.get<Offer[]>(`${ApiRoutes.Offers}/${id}/nearby`); + const { data } = await api.get<Offer[]>(`${ApiRoute.Offers}/${id}/nearby`); dispatch(setNearbyOffers(data)); }); @@ -79,7 +79,7 @@ export const fetchReviews = createAsyncThunk< extra: AxiosInstance; } >('review/fetchReviews', async (offerId, { dispatch, extra: api }) => { - const { data } = await api.get<Review[]>(`${ApiRoutes.Comments}/${offerId}`); + const { data } = await api.get<Review[]>(`${ApiRoute.Comments}/${offerId}`); dispatch(setCurrentReviews(data)); }); @@ -93,7 +93,7 @@ export const postReview = createAsyncThunk< } >('review/postReview', async (info, { dispatch, extra: api }) => { try { - const response = await api.post(`${ApiRoutes.Comments}/${info.offerId}`, { + const response = await api.post(`${ApiRoute.Comments}/${info.offerId}`, { comment: info.comment, rating: info.rating, }); @@ -148,7 +148,7 @@ export const fetchFavoriteOffers = createAsyncThunk< extra: AxiosInstance; } >('offers/fetchFavorites', async (_arg, { dispatch, extra: api }) => { - const response = await api.get<Offer[]>(ApiRoutes.Favorites); + const response = await api.get<Offer[]>(ApiRoute.Favorites); if (response.status === 200) { dispatch(setFavoriteOffers(response.data)); } @@ -164,7 +164,7 @@ export const bookmarkOffer = createAsyncThunk< } >('review/fetchReviews', async (info, { dispatch, extra: api }) => { const response = await api.post( - `${ApiRoutes.Favorites}/${info.offerId}/${+info.status}`, + `${ApiRoute.Favorites}/${info.offerId}/${+info.status}`, ); if (response.status === 201 || response.status === 200) { dispatch(fetchFavoriteOffers()); @@ -181,7 +181,7 @@ export const login = createAsyncThunk< } >('auth/login', async (loginInfo, { dispatch, extra: api }) => { try { - const response = await api.post<AuthInfo>(ApiRoutes.Login, loginInfo); + const response = await api.post<AuthInfo>(ApiRoute.Login, loginInfo); if (response.status === 200 || response.status === 201) { dispatch(setAuthorizationStatus(AuthorizationStatus.Authorized)); saveToken(response.data.token); @@ -210,7 +210,7 @@ export const checkAuthorization = createAsyncThunk< } >('auth/checkAuthorization', async (_arg, { dispatch, extra: api }) => { try { - const response = await api.get<AuthInfo>(ApiRoutes.Login); + const response = await api.get<AuthInfo>(ApiRoute.Login); if (response.status === 200 || response.status === 201) { dispatch(setAuthorizationStatus(AuthorizationStatus.Authorized)); dispatch(setUserInfo(response.data)); @@ -237,6 +237,6 @@ export const logout = createAsyncThunk< extra: AxiosInstance; } >('auth/logout', async (_arg, { dispatch, extra: api }) => { - await api.delete(ApiRoutes.Logout); + await api.delete(ApiRoute.Logout); dispatch(setAuthorizationStatus(AuthorizationStatus.Unauthorized)); }); diff --git a/src/store/current-offer/current-offer.selectors.ts b/src/store/current-offer/current-offer.selectors.ts index bd74d1e..735ab74 100644 --- a/src/store/current-offer/current-offer.selectors.ts +++ b/src/store/current-offer/current-offer.selectors.ts @@ -1,22 +1,24 @@ import { State } from '../../dataTypes/store-types.ts'; -import { NameSpaces } from '../../dataTypes/enums/name-spaces.ts'; +import { NameSpace } from '../../dataTypes/enums/name-space.ts'; import { MAX_NEARBY_OFFERS } from '../../consts/offers.ts'; import { createSelector } from '@reduxjs/toolkit'; import { Offer } from '../../dataTypes/offer.ts'; import { Review } from '../../dataTypes/review.ts'; -export const getCurrentOffer = (state: State) => - state[NameSpaces.CurrentOffer].currentOffer; +type CurrentOfferState = Pick<State, NameSpace.CurrentOffer>; + +export const getCurrentOffer = (state: CurrentOfferState) => + state[NameSpace.CurrentOffer].currentOffer; export const getNearbyOffers = createSelector( - [(state: State) => state[NameSpaces.CurrentOffer].nearbyOffers], + [(state: CurrentOfferState) => state[NameSpace.CurrentOffer].nearbyOffers], (offers: Offer[]) => offers.slice(0, MAX_NEARBY_OFFERS), ); -export const getCurrentReviews = (state: State) => - state[NameSpaces.CurrentOffer].currentReviews; +export const getCurrentReviews = (state: CurrentOfferState) => + state[NameSpace.CurrentOffer].currentReviews; export const getReviewsCount = createSelector( - [(state: State) => state[NameSpaces.CurrentOffer].currentReviews], + [(state: CurrentOfferState) => state[NameSpace.CurrentOffer].currentReviews], (reviews: Review[]) => reviews.length, ); -export const getReviewPostingStatus = (state: State) => - state[NameSpaces.CurrentOffer].reviewPostingStatus; +export const getReviewPostingStatus = (state: CurrentOfferState) => + state[NameSpace.CurrentOffer].reviewPostingStatus; diff --git a/src/store/current-offer/current-offer.slice.test.ts b/src/store/current-offer/current-offer.slice.test.ts new file mode 100644 index 0000000..ad14d39 --- /dev/null +++ b/src/store/current-offer/current-offer.slice.test.ts @@ -0,0 +1,80 @@ +import { ReviewStatus } from '../../dataTypes/enums/review-status.ts'; +import { + currentOfferSlice, + setCurrentOffer, + setCurrentReviews, + setNearbyOffers, + setReviewPostingStatus, +} from './current-offer.slice.ts'; +import { getMockDetailedOffer } from '../../mocks/mock-detailed-offer.ts'; +import { describe, expect, it } from 'vitest'; +import { getMockOffers } from '../../mocks/mock-offers.ts'; +import { getMockReviews } from '../../mocks/mock-review.ts'; + +describe('current offer slice tests', () => { + const initialState = { + currentOffer: null, + nearbyOffers: [], + currentReviews: [], + reviewPostingStatus: ReviewStatus.Success, + }; + it('should return initial state with empty action', () => { + const emptyAction = { type: '' }; + + const result = currentOfferSlice.reducer(initialState, emptyAction); + + expect(result).toEqual(initialState); + }); + + it('should return default initial state with empty action', () => { + const emptyAction = { type: '' }; + + const result = currentOfferSlice.reducer(undefined, emptyAction); + + expect(result).toEqual(initialState); + }); + + it('should set currentOffer', () => { + const detailedOffer = getMockDetailedOffer(); + + const newState = currentOfferSlice.reducer( + initialState, + setCurrentOffer(detailedOffer), + ); + + expect(newState.currentOffer).toEqual(detailedOffer); + }); + + it('should set nearbyOffers', () => { + const nearbyOffers = getMockOffers(2); + + const newState = currentOfferSlice.reducer( + initialState, + setNearbyOffers(nearbyOffers), + ); + + expect(newState.nearbyOffers).toEqual(nearbyOffers); + }); + + it('should set currentReviews', () => { + const reviews = getMockReviews(3); + + const newState = currentOfferSlice.reducer( + initialState, + setCurrentReviews(reviews), + ); + + expect(newState.currentReviews).toEqual(reviews); + }); + + it('should set reviewPostingStatus', () => { + const status = ReviewStatus.Success; + + const newState = currentOfferSlice.reducer( + initialState, + setReviewPostingStatus(status), + ); + + expect(newState.reviewPostingStatus).toEqual(status); + }); +}); diff --git a/src/store/current-offer/current-offer.slice.ts b/src/store/current-offer/current-offer.slice.ts index 04a9ad5..9638322 100644 --- a/src/store/current-offer/current-offer.slice.ts +++ b/src/store/current-offer/current-offer.slice.ts @@ -1,10 +1,10 @@ import { Offer } from '../../dataTypes/offer.ts'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { NameSpaces } from '../../dataTypes/enums/name-spaces.ts'; -import { Nullable } from 'vitest'; +import { NameSpace } from '../../dataTypes/enums/name-space.ts'; import { Review } from '../../dataTypes/review.ts'; import { DetailedOffer } from '../../dataTypes/detailed-offer.ts'; import { ReviewStatus } from '../../dataTypes/enums/review-status.ts'; +import { Nullable } from 'vitest'; type CurrentOfferInitialState = { currentOffer: Nullable<DetailedOffer>; @@ -21,7 +21,7 @@ const initialState: CurrentOfferInitialState = { }; export const currentOfferSlice = createSlice({ - name: NameSpaces.CurrentOffer, + name: NameSpace.CurrentOffer, initialState, reducers: { setCurrentOffer: ( diff --git a/src/store/current-offer/current-offers.selectors.test.ts b/src/store/current-offer/current-offers.selectors.test.ts new file mode 100644 index 0000000..8f3eab7 --- /dev/null +++ b/src/store/current-offer/current-offers.selectors.test.ts @@ -0,0 +1,48 @@ +import { describe } from 'vitest'; +import { + getCurrentOffer, + getCurrentReviews, + getNearbyOffers, + getReviewPostingStatus, + getReviewsCount, +} from './current-offer.selectors.ts'; +import { ReviewStatus } from '../../dataTypes/enums/review-status.ts'; +import { getMockDetailedOffer } from '../../mocks/mock-detailed-offer.ts'; +import { getMockOffers } from '../../mocks/mock-offers.ts'; +import { getMockReviews } from '../../mocks/mock-review.ts'; +import { NameSpace } from '../../dataTypes/enums/name-space.ts'; + +describe('Current offer selectors test', () => { + const state = { + [NameSpace.CurrentOffer]: { + currentOffer: getMockDetailedOffer(), + nearbyOffers: getMockOffers(3), + currentReviews: getMockReviews(2), + reviewPostingStatus: ReviewStatus.Success, + }, + }; + it('should return current offer', () => { + const result = getCurrentOffer(state); + expect(result).toEqual(state[NameSpace.CurrentOffer].currentOffer); + }); + + it('should return nearby offers', () => { + const result = getNearbyOffers(state); + expect(result).toEqual(state[NameSpace.CurrentOffer].nearbyOffers); + }); + + it('should return current reviews', () => { + const result = getCurrentReviews(state); + expect(result).toEqual(state[NameSpace.CurrentOffer].currentReviews); + }); + + it('should return number of reviews', () => { + const result = getReviewsCount(state); + expect(result).toBe(2); + }); + + it('should return review posting status', () => { + const result = getReviewPostingStatus(state); + expect(result).toBe(state[NameSpace.CurrentOffer].reviewPostingStatus); + }); +}); diff --git a/src/store/offers/offers.selector.test.ts b/src/store/offers/offers.selector.test.ts new file mode 100644 index 0000000..d9db6d5 --- /dev/null +++ b/src/store/offers/offers.selector.test.ts @@ -0,0 +1,37 @@ +import { + getFavoritesOffers, + getCity, + getSortedOffers, +} from './offers.selectors.ts'; +import { NameSpace } from '../../dataTypes/enums/name-space.ts'; +import { AMSTERDAM } from '../../consts/cities.ts'; +import { Offer } from '../../dataTypes/offer.ts'; +import { getMockOffers } from '../../mocks/mock-offers.ts'; + +describe('offers selectors test', () => { + const state = { + [NameSpace.Offers]: { + city: AMSTERDAM, + offers: getMockOffers(3), + sorting: (offers: Offer[]) => + offers.toSorted((a, b) => a.price - b.price), + favoritesOffers: getMockOffers(4), + }, + }; + + it('should return favorite offers', () => { + const result = getFavoritesOffers(state); + expect(result).toEqual(state[NameSpace.Offers].favoritesOffers); + }); + + it('should return city', () => { + const result = getCity(state); + expect(result).toEqual(AMSTERDAM); + }); + + it('should return sorted offers for the city', () => { + const sortedOffers = getSortedOffers(state); + expect(sortedOffers[0].price).toBeLessThan(sortedOffers[1].price); + expect(sortedOffers[1].price).toBeLessThan(sortedOffers[2].price); + }); +}); diff --git a/src/store/offers/offers.selectors.ts b/src/store/offers/offers.selectors.ts index 204833e..275e7ef 100644 --- a/src/store/offers/offers.selectors.ts +++ b/src/store/offers/offers.selectors.ts @@ -1,18 +1,20 @@ import { State } from '../../dataTypes/store-types.ts'; -import { NameSpaces } from '../../dataTypes/enums/name-spaces.ts'; +import { NameSpace } from '../../dataTypes/enums/name-space.ts'; import { createSelector } from '@reduxjs/toolkit'; import { Offer } from '../../dataTypes/offer.ts'; import { City } from '../../dataTypes/city.ts'; import { SortOffers } from '../../dataTypes/sort-offers.ts'; -export const getFavoritesOffers = (state: State) => - state[NameSpaces.Offers].favoritesOffers; -export const getCity = (state: State) => state[NameSpaces.Offers].city; +type OffersState = Pick<State, NameSpace.Offers>; + +export const getFavoritesOffers = (state: OffersState) => + state[NameSpace.Offers].favoritesOffers; +export const getCity = (state: OffersState) => state[NameSpace.Offers].city; export const getSortedOffers = createSelector( [ - (state: State) => state[NameSpaces.Offers].offers, - (state: State) => state[NameSpaces.Offers].city, - (state: State) => state[NameSpaces.Offers].sorting, + (state: OffersState) => state[NameSpace.Offers].offers, + (state: OffersState) => state[NameSpace.Offers].city, + (state: OffersState) => state[NameSpace.Offers].sorting, ], (offers: Offer[], city: City, sort: SortOffers) => sort(offers.filter((offer: Offer) => offer.city.name === city.name)), diff --git a/src/store/offers/offers.slice.test.ts b/src/store/offers/offers.slice.test.ts new file mode 100644 index 0000000..f78149f --- /dev/null +++ b/src/store/offers/offers.slice.test.ts @@ -0,0 +1,57 @@ +import { + offersSlice, + changeCity, + setOffers, + setSorting, + setFavoriteOffers, +} from './offers.slice.ts'; +import { Offer } from '../../dataTypes/offer.ts'; +import { AMSTERDAM, PARIS } from '../../consts/cities.ts'; +import { getMockOffers } from '../../mocks/mock-offers.ts'; +import { expect, it } from 'vitest'; + +describe('offers slice test', () => { + const initialState = { + city: PARIS, + offers: [], + sorting: (offers: Offer[]): Offer[] => offers, + favoritesOffers: [], + }; + it('should return initial state with empty action', () => { + const emptyAction = { type: '' }; + + const result = offersSlice.reducer(initialState, emptyAction); + + expect(result).toEqual(initialState); + }); + + it('should change city', () => { + const newState = offersSlice.reducer(initialState, changeCity(AMSTERDAM)); + expect(newState.city).toEqual(AMSTERDAM); + }); + + it('should set offers', () => { + const newOffers = getMockOffers(3); + const newState = offersSlice.reducer(initialState, setOffers(newOffers)); + expect(newState.offers).toEqual(newOffers); + }); + + it('should set sorting function', () => { + const sortingFunction = (offers: Offer[]) => + offers.sort((a, b) => a.price - b.price); + const newState = offersSlice.reducer( + initialState, + setSorting(sortingFunction), + ); + expect(newState.sorting).toEqual(sortingFunction); + }); + + it('should set favorite offers', () => { + const favoriteOffers = getMockOffers(3); + const newState = offersSlice.reducer( + initialState, + setFavoriteOffers(favoriteOffers), + ); + expect(newState.favoritesOffers).toEqual(favoriteOffers); + }); +}); diff --git a/src/store/offers/offers.slice.ts b/src/store/offers/offers.slice.ts index a822b7c..26cd284 100644 --- a/src/store/offers/offers.slice.ts +++ b/src/store/offers/offers.slice.ts @@ -3,7 +3,7 @@ import { Offer } from '../../dataTypes/offer.ts'; import { City } from '../../dataTypes/city.ts'; import { SortOffers } from '../../dataTypes/sort-offers.ts'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { NameSpaces } from '../../dataTypes/enums/name-spaces.ts'; +import { NameSpace } from '../../dataTypes/enums/name-space.ts'; type OffersInitialState = { city: City; offers: Offer[]; @@ -19,7 +19,7 @@ const initialState: OffersInitialState = { }; export const offersSlice = createSlice({ - name: NameSpaces.Offers, + name: NameSpace.Offers, initialState, reducers: { changeCity: (state, action: PayloadAction<City>) => { diff --git a/src/store/store.ts b/src/store/store.ts index 8de9388..8b6d392 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -2,7 +2,7 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'; import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { AppDispatch, State } from '../dataTypes/store-types.ts'; import { createAPI } from '../api/api.ts'; -import { NameSpaces } from '../dataTypes/enums/name-spaces.ts'; +import { NameSpace } from '../dataTypes/enums/name-space.ts'; import { offersSlice } from './offers/offers.slice.ts'; import { userSlice } from './user/user-slice.ts'; import { currentOfferSlice } from './current-offer/current-offer.slice.ts'; @@ -10,9 +10,9 @@ import { currentOfferSlice } from './current-offer/current-offer.slice.ts'; export const api = createAPI(); const reducer = combineReducers({ - [NameSpaces.Offers]: offersSlice.reducer, - [NameSpaces.CurrentOffer]: currentOfferSlice.reducer, - [NameSpaces.User]: userSlice.reducer, + [NameSpace.Offers]: offersSlice.reducer, + [NameSpace.CurrentOffer]: currentOfferSlice.reducer, + [NameSpace.User]: userSlice.reducer, }); export const store = configureStore({ diff --git a/src/store/user/user-slice.ts b/src/store/user/user-slice.ts index dd88c91..6e847ce 100644 --- a/src/store/user/user-slice.ts +++ b/src/store/user/user-slice.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { NameSpaces } from '../../dataTypes/enums/name-spaces.ts'; +import { NameSpace } from '../../dataTypes/enums/name-space.ts'; import { AuthorizationStatus } from '../../dataTypes/enums/authorization-status.ts'; import { AuthInfo } from '../../dataTypes/user.ts'; @@ -14,7 +14,7 @@ const initialState: UserInitialState = { }; export const userSlice = createSlice({ - name: NameSpaces.User, + name: NameSpace.User, initialState, reducers: { setAuthorizationStatus: ( diff --git a/src/store/user/user.selector.test.ts b/src/store/user/user.selector.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/store/user/user.selectors.ts b/src/store/user/user.selectors.ts index 3c1b9f3..c6014a6 100644 --- a/src/store/user/user.selectors.ts +++ b/src/store/user/user.selectors.ts @@ -1,7 +1,9 @@ import { State } from '../../dataTypes/store-types.ts'; -import { NameSpaces } from '../../dataTypes/enums/name-spaces.ts'; +import { NameSpace } from '../../dataTypes/enums/name-space.ts'; import { AuthorizationStatus } from '../../dataTypes/enums/authorization-status.ts'; -export const getIsAuthorized = (state: State) => - state[NameSpaces.User].authorizationStatus === AuthorizationStatus.Authorized; -export const getUserInfo = (state: State) => state[NameSpaces.User].userInfo; +type UserState = Pick<State, NameSpace.User>; + +export const getIsAuthorized = (state: UserState) => + state[NameSpace.User].authorizationStatus === AuthorizationStatus.Authorized; +export const getUserInfo = (state: UserState) => state[NameSpace.User].userInfo; diff --git a/src/store/user/user.slice.test.ts b/src/store/user/user.slice.test.ts new file mode 100644 index 0000000..e69de29 From 1599c3b1b4757c40efe7cc963a0e05ab79fa36e5 Mon Sep 17 00:00:00 2001 From: Gurikov Maxim <maximgurikoff@gmail.com> Date: Tue, 10 Dec 2024 23:30:22 +0500 Subject: [PATCH 3/5] fixes --- src/store/user/user.selector.test.ts | 23 ++++++++++++++ src/store/user/user.slice.test.ts | 47 ++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/store/user/user.selector.test.ts b/src/store/user/user.selector.test.ts index e69de29..2cd8fb8 100644 --- a/src/store/user/user.selector.test.ts +++ b/src/store/user/user.selector.test.ts @@ -0,0 +1,23 @@ +import { getIsAuthorized, getUserInfo } from './user.selectors.ts'; +import { NameSpace } from '../../dataTypes/enums/name-space.ts'; +import { getMockAuthInfo } from '../../mocks/mock-user.ts'; +import { AuthorizationStatus } from '../../dataTypes/enums/authorization-status.ts'; + +describe('user selectors test', () => { + const state = { + [NameSpace.User]: { + authorizationStatus: AuthorizationStatus.Authorized, + userInfo: getMockAuthInfo(), + }, + }; + + it('should return true if user is authorized', () => { + const result = getIsAuthorized(state); + expect(result).toBe(true); + }); + + it('should return user info', () => { + const result = getUserInfo(state); + expect(result).toEqual(state[NameSpace.User].userInfo); + }); +}); diff --git a/src/store/user/user.slice.test.ts b/src/store/user/user.slice.test.ts index e69de29..0051404 100644 --- a/src/store/user/user.slice.test.ts +++ b/src/store/user/user.slice.test.ts @@ -0,0 +1,47 @@ +import { + setAuthorizationStatus, + setUserInfo, + userSlice, +} from './user-slice.ts'; +import { AuthorizationStatus } from '../../dataTypes/enums/authorization-status.ts'; +import { getMockAuthInfo } from '../../mocks/mock-user.ts'; +import { expect, it } from 'vitest'; +import { currentOfferSlice } from '../current-offer/current-offer.slice.ts'; + +describe('user slice test', () => { + const initialState = { + authorizationStatus: AuthorizationStatus.Unknown, + userInfo: null, + }; + it('should return initial state with empty action', () => { + const emptyAction = { type: '' }; + + const result = userSlice.reducer(initialState, emptyAction); + + expect(result).toEqual(initialState); + }); + + it('should return default initial state with empty action', () => { + const emptyAction = { type: '' }; + + const result = userSlice.reducer(undefined, emptyAction); + + expect(result).toEqual(initialState); + }); + + it('should set authorization status', () => { + const newState = userSlice.reducer( + initialState, + setAuthorizationStatus(AuthorizationStatus.Authorized), + ); + expect(newState.authorizationStatus).toEqual( + AuthorizationStatus.Authorized, + ); + }); + + it('should set user info', () => { + const userInfo = getMockAuthInfo(); + const newState = userSlice.reducer(initialState, setUserInfo(userInfo)); + expect(newState.userInfo).toEqual(userInfo); + }); +}); From 1a91503b73ba0a3a5a9a4f50f701fec88e664504 Mon Sep 17 00:00:00 2001 From: Gurikov Maxim <maximgurikoff@gmail.com> Date: Wed, 11 Dec 2024 20:26:48 +0500 Subject: [PATCH 4/5] fix --- src/store/offers/offers.slice.test.ts | 10 ++++++++++ src/store/user/user.slice.test.ts | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/store/offers/offers.slice.test.ts b/src/store/offers/offers.slice.test.ts index f78149f..550a438 100644 --- a/src/store/offers/offers.slice.test.ts +++ b/src/store/offers/offers.slice.test.ts @@ -25,6 +25,16 @@ describe('offers slice test', () => { expect(result).toEqual(initialState); }); + it('should return default initial state with empty action', () => { + const emptyAction = { type: '' }; + + const result = offersSlice.reducer(undefined, emptyAction); + + expect(result.city.name).toEqual(initialState.city.name); + expect(result.offers).toEqual([]); + expect(result.favoritesOffers).toEqual([]); + }); + it('should change city', () => { const newState = offersSlice.reducer(initialState, changeCity(AMSTERDAM)); expect(newState.city).toEqual(AMSTERDAM); diff --git a/src/store/user/user.slice.test.ts b/src/store/user/user.slice.test.ts index 0051404..b16fb7a 100644 --- a/src/store/user/user.slice.test.ts +++ b/src/store/user/user.slice.test.ts @@ -6,7 +6,6 @@ import { AuthorizationStatus } from '../../dataTypes/enums/authorization-status.ts'; import { getMockAuthInfo } from '../../mocks/mock-user.ts'; import { expect, it } from 'vitest'; -import { currentOfferSlice } from '../current-offer/current-offer.slice.ts'; describe('user slice test', () => { const initialState = { From 4c03d8b69d23c0a26855c21f320fc73ef1ddea07 Mon Sep 17 00:00:00 2001 From: Gurikov Maxim <maximgurikoff@gmail.com> Date: Wed, 11 Dec 2024 20:55:55 +0500 Subject: [PATCH 5/5] more fixes --- src/Pages/login-page/login-page.tsx | 6 ++---- src/components/offer/offer-card.tsx | 2 +- src/components/reviews/review-component.tsx | 3 ++- src/components/reviews/review-form.tsx | 3 ++- src/consts/cities.ts | 2 +- src/utils/date-utils.ts | 22 +++++++++++++++++++++ 6 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 src/utils/date-utils.ts diff --git a/src/Pages/login-page/login-page.tsx b/src/Pages/login-page/login-page.tsx index 7482064..26a5633 100644 --- a/src/Pages/login-page/login-page.tsx +++ b/src/Pages/login-page/login-page.tsx @@ -46,8 +46,7 @@ export function LoginPage(): React.JSX.Element { name="email" placeholder="Email" onChange={(event) => - setLoginInfo({ ...loginInfo, email: event.target.value }) - } + setLoginInfo({ ...loginInfo, email: event.target.value })} required /> </div> @@ -62,8 +61,7 @@ export function LoginPage(): React.JSX.Element { setLoginInfo({ ...loginInfo, password: event.target.value, - }) - } + })} required /> </div> diff --git a/src/components/offer/offer-card.tsx b/src/components/offer/offer-card.tsx index 71c150c..5950e6b 100644 --- a/src/components/offer/offer-card.tsx +++ b/src/components/offer/offer-card.tsx @@ -38,7 +38,7 @@ export function OfferCardImpl({ onMouseLeave={handleMouseLeave} className={cn( 'place-card', - { cities__card: isOnMainPage }, + { 'cities__card': isOnMainPage }, { 'near-places__card': !isOnMainPage }, )} > diff --git a/src/components/reviews/review-component.tsx b/src/components/reviews/review-component.tsx index 1e72ef5..32d3263 100644 --- a/src/components/reviews/review-component.tsx +++ b/src/components/reviews/review-component.tsx @@ -1,5 +1,6 @@ import { getFirstName } from '../../utils/username-utils.ts'; import { Rating } from '../rating.tsx'; +import { formatDate } from '../../utils/date-utils.ts'; interface ReviewProps { comment: string; @@ -34,7 +35,7 @@ export function ReviewComponent({ <Rating rating={rating} usePlace="reviews" /> <p className="reviews__text">{comment}</p> <time className="reviews__time" dateTime={date.toDateString()}> - {date.toLocaleDateString('en-US', {})} + {formatDate(date)} </time> </div> </li> diff --git a/src/components/reviews/review-form.tsx b/src/components/reviews/review-form.tsx index 4c2df7b..a527aca 100644 --- a/src/components/reviews/review-form.tsx +++ b/src/components/reviews/review-form.tsx @@ -170,7 +170,8 @@ export function ReviewForm(): React.JSX.Element { value={review?.comment || ''} onChange={onCommentChange} disabled={reviewPostingStatus === ReviewStatus.Pending} - ></textarea> + > + </textarea> <div className="reviews__button-wrapper"> <p className="reviews__help"> To submit review please make sure to set{' '} diff --git a/src/consts/cities.ts b/src/consts/cities.ts index dfee7ec..f28474b 100644 --- a/src/consts/cities.ts +++ b/src/consts/cities.ts @@ -61,4 +61,4 @@ export const CITIES: City[] = [ AMSTERDAM, HAMBURG, DUSSELDORF, -]; +] as const; diff --git a/src/utils/date-utils.ts b/src/utils/date-utils.ts new file mode 100644 index 0000000..5ab91a0 --- /dev/null +++ b/src/utils/date-utils.ts @@ -0,0 +1,22 @@ +export function formatDate(dateString: Date): string { + const monthsInEnglish = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + + const date = new Date(dateString); + const month = monthsInEnglish[date.getMonth()]; + const year = date.getFullYear(); + + return `${month} ${year}`; +}