Skip to content

Commit

Permalink
Merge pull request #19 from antoshkaxxr/master
Browse files Browse the repository at this point in the history
  • Loading branch information
keksobot authored Dec 21, 2024
2 parents f2be5fa + 02f9242 commit f2cbe17
Show file tree
Hide file tree
Showing 26 changed files with 509 additions and 65 deletions.
30 changes: 19 additions & 11 deletions src/components/card/card.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,43 @@
import {Offer} from '../../types/offer.ts';
import {Link} from 'react-router-dom';
import {AppRoute, AuthorizationStatus} from '../../const.ts';
import {memo, useCallback} from 'react';
import {useCallback} from 'react';
import {useAppDispatch, useAppSelector} from '../../hooks';
import {changeFavoriteAction} from '../../store/api-actions.ts';
import {changeFavoriteAction, fetchNearbyOffersAction} from '../../store/api-actions.ts';
import {getAuthorizationStatus} from '../../store/user-data/selectors.ts';
import {redirectToRoute} from '../../store/action.ts';
import {showCustomToast} from '../../utils/show-custom-toast.tsx';

type CardProps = {
offer: Offer;
onMouseEnter: () => void;
onMouseLeave: () => void;
isNearby?: boolean;
parentOfferId?: string;
}

function CardComponent({offer, onMouseEnter, onMouseLeave, isNearby = false}: CardProps) {
const stylePrefix = isNearby ? 'near-places' : 'cities';
export function Card({offer, onMouseEnter, onMouseLeave, parentOfferId = undefined}: CardProps) {
const stylePrefix = parentOfferId ? 'near-places' : 'cities';
const dispatch = useAppDispatch();
const authorizationStatus = useAppSelector(getAuthorizationStatus);

const handleFavoriteClick = useCallback(() => {
const handleFavoriteClick = useCallback(async () => {
if (authorizationStatus !== AuthorizationStatus.Auth) {
dispatch(redirectToRoute(AppRoute.Login));
return;
}

const newStatus = offer.isFavorite ? 0 : 1;
dispatch(changeFavoriteAction({ offerId: offer.id, status: newStatus }));
}, [authorizationStatus, offer.isFavorite, offer.id, dispatch]);
await dispatch(changeFavoriteAction({offerId: offer.id, status: newStatus}));
if (parentOfferId) {
dispatch(fetchNearbyOffersAction(parentOfferId));
}
}, [authorizationStatus, offer.isFavorite, offer.id, dispatch, parentOfferId]);

const handleClickWrapper = () => {
handleFavoriteClick().catch((error) => {
showCustomToast(`${error}`);
});
};

return (
<article
Expand All @@ -53,7 +63,7 @@ function CardComponent({offer, onMouseEnter, onMouseLeave, isNearby = false}: Ca
<button
className={`place-card__bookmark-button button ${offer.isFavorite && 'place-card__bookmark-button--active '}button`}
type="button"
onClick={handleFavoriteClick}
onClick={handleClickWrapper} // Используем обёртку
>
<svg className="place-card__bookmark-icon" width="18" height="19">
<use xlinkHref="#icon-bookmark"></use>
Expand All @@ -77,5 +87,3 @@ function CardComponent({offer, onMouseEnter, onMouseLeave, isNearby = false}: Ca
</article>
);
}

export const Card = memo(CardComponent);
34 changes: 24 additions & 10 deletions src/components/cities-list/cities-list.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BrowserRouter } from 'react-router-dom';
import { CitiesList } from './cities-list';

describe('Component: CitiesList', () => {
Expand All @@ -9,11 +10,13 @@ describe('Component: CitiesList', () => {

it('should render correctly', () => {
render(
<CitiesList
cities={mockCities}
activeCity={mockActiveCity}
onCityChange={mockOnCityChange}
/>
<BrowserRouter>
<CitiesList
cities={mockCities}
activeCity={mockActiveCity}
onCityChange={mockOnCityChange}
/>
</BrowserRouter>
);

mockCities.forEach((city) => {
Expand All @@ -22,15 +25,24 @@ describe('Component: CitiesList', () => {

const activeCityLink = screen.getByRole('link', { name: mockActiveCity });
expect(activeCityLink).toHaveClass('tabs__item--active');

mockCities
.filter((city) => city !== mockActiveCity)
.forEach((city) => {
const cityLink = screen.getByRole('link', { name: city });
expect(cityLink).not.toHaveClass('tabs__item--active');
});
});

it('should call onCityChange with the correct city when a city is clicked', async () => {
render(
<CitiesList
cities={mockCities}
activeCity={mockActiveCity}
onCityChange={mockOnCityChange}
/>
<BrowserRouter>
<CitiesList
cities={mockCities}
activeCity={mockActiveCity}
onCityChange={mockOnCityChange}
/>
</BrowserRouter>
);

const cityToClick = 'Cologne';
Expand All @@ -39,5 +51,7 @@ describe('Component: CitiesList', () => {
await userEvent.click(cityLink);

expect(mockOnCityChange).toHaveBeenCalledWith(cityToClick);

expect(mockOnCityChange).toHaveBeenCalledTimes(1);
});
});
8 changes: 5 additions & 3 deletions src/components/cities-list/cities-list.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {memo} from 'react';
import {Link} from 'react-router-dom';

type CitiesListProps = {
cities: string[];
Expand All @@ -12,16 +13,17 @@ function CitiesListComponent({ cities, activeCity, onCityChange }: CitiesListPro
<ul className="locations__list tabs__list">
{cities.map((city) => (
<li className="locations__item" key={city}>
<a
<Link
className={`locations__item-link tabs__item ${city === activeCity ? 'tabs__item--active' : ''}`}
href="#"
to="#"
onClick={(e) => {
e.preventDefault();
onCityChange(city);
}}
role={'link'}
>
<span>{city}</span>
</a>
</Link>
</li>
))}
</ul>
Expand Down
2 changes: 1 addition & 1 deletion src/components/comment-form/comment-form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, {ChangeEvent, FormEvent, memo, useCallback, useState} from 'react';
import {useAppDispatch} from '../../hooks';
import {postCommentAction} from '../../store/api-actions.ts';
import {showCustomToast} from '../custom-toast/custom-toast.tsx';
import {showCustomToast} from '../../utils/show-custom-toast.tsx';

const getRatingTitle = (star: number) => {
switch (star) {
Expand Down
3 changes: 2 additions & 1 deletion src/components/custom-toast/custom-toast.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { render, screen } from '@testing-library/react';
import { toast } from 'react-toastify';
import {showCustomToast, CustomToastContainer, CustomToast} from './custom-toast';
import {CustomToastContainer, CustomToast} from './custom-toast';
import {showCustomToast} from '../../utils/show-custom-toast.tsx';

vi.mock('react-toastify', () => ({
toast: vi.fn(),
Expand Down
6 changes: 1 addition & 5 deletions src/components/custom-toast/custom-toast.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { ToastContainer, toast } from 'react-toastify';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

export const CustomToast: React.FC<{ message: string }> = ({ message }) => (
Expand All @@ -8,10 +8,6 @@ export const CustomToast: React.FC<{ message: string }> = ({ message }) => (
</div>
);

export const showCustomToast = (message: string) => {
toast(<CustomToast message={message} />);
};

export function CustomToastContainer() {
return (
<ToastContainer
Expand Down
6 changes: 3 additions & 3 deletions src/components/favorite-card/favorite-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function FavoriteCardComponent({offer}: FavoriteCardProps) {

const handleFavoriteClick = useCallback(() => {
dispatch(changeFavoriteAction({ offerId: offer.id, status: 0 }));
}, [offer.isFavorite, offer.id, dispatch]);
}, [offer.id, dispatch]);

return (
<article className={'favorites__card place-card'}>
Expand All @@ -23,9 +23,9 @@ function FavoriteCardComponent({offer}: FavoriteCardProps) {
<span>Premium</span>
</div>}
<div className={'favorites__image-wrapper place-card__image-wrapper'}>
<a href="#">
<Link to={`${AppRoute.Offer}/${offer.id}`}>
<img className="place-card__image" src={offer.previewImage} width="150" height="110" alt="Place image"/>
</a>
</Link>
</div>
<div className={'favorites__card-info place-card__info'}>
<div className="place-card__price-wrapper">
Expand Down
22 changes: 19 additions & 3 deletions src/components/favorites-list/favorites-list.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import { configureMockStore } from '@jedmao/redux-mock-store';
import { FavoritesList } from './favorites-list';
import { Offer } from '../../types/offer';
import {BrowserRouter} from 'react-router-dom';

vi.mock('../favorite-card/favorite-card', () => ({
FavoriteCard: vi.fn(({ offer }: {offer: Offer}) => <div data-testid="favorite-card">{offer.title}</div>),
FavoriteCard: vi.fn(({ offer }: { offer: Offer }) => <div data-testid="favorite-card">{offer.title}</div>),
}));

const mockStore = configureMockStore();
const store = mockStore({});

describe('Component: FavoritesList', () => {
const mockOffers: Offer[] = [
{
Expand Down Expand Up @@ -47,7 +53,13 @@ describe('Component: FavoritesList', () => {
];

it('should render grouped offers by city', () => {
render(<FavoritesList offers={mockOffers} />);
render(
<Provider store={store}>
<BrowserRouter>
<FavoritesList offers={mockOffers} />
</BrowserRouter>
</Provider>
);

expect(screen.getByText('Paris')).toBeInTheDocument();
expect(screen.getByText('Amsterdam')).toBeInTheDocument();
Expand All @@ -64,7 +76,11 @@ describe('Component: FavoritesList', () => {
});

it('should render no offers if offers array is empty', () => {
render(<FavoritesList offers={[]} />);
render(
<Provider store={store}>
<FavoritesList offers={[]} />
</Provider>
);

expect(screen.queryByText(/Paris|Amsterdam/)).not.toBeInTheDocument();
expect(screen.queryByTestId('favorite-card')).not.toBeInTheDocument();
Expand Down
20 changes: 17 additions & 3 deletions src/components/favorites-list/favorites-list.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import {useMemo} from 'react';
import {useCallback, useMemo} from 'react';
import {Offer} from '../../types/offer.ts';
import {FavoriteCard} from '../favorite-card/favorite-card.tsx';
import {Link} from 'react-router-dom';
import {AppRoute} from '../../const.ts';
import {setCity} from '../../store/slices/city-slice.ts';
import {useAppDispatch} from '../../hooks';

type FavoritesListProps = {
offers: Offer[];
Expand All @@ -19,15 +23,25 @@ export function FavoritesList({offers}: FavoritesListProps) {
return acc;
}, {} as Record<string, Offer[]>), [offers]);

const dispatch = useAppDispatch();

const handleCityClick = useCallback((city: string) => {
dispatch(setCity(city));
}, [dispatch]);

return (
<ul className="favorites__list">
{Object.entries(groupedOffers).map(([city, cityOffers]) => (
<li className="favorites__locations-items" key={city}>
<div className="favorites__locations locations locations--current">
<div className="locations__item">
<a className="locations__item-link" href="#">
<Link
className="locations__item-link"
to={AppRoute.Main}
onClick={() => handleCityClick(city)}
>
<span>{city}</span>
</a>
</Link>
</div>
</div>
<div className={'favorites__places'}>
Expand Down
6 changes: 5 additions & 1 deletion src/components/login-form/login-form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './login-form';
import {loginAction} from '../../store/api-actions';
import { showCustomToast } from '../custom-toast/custom-toast';
import {withHistory, withStore} from '../../utils/mock-component.tsx';
import {showCustomToast} from '../../utils/show-custom-toast.tsx';

vi.mock('../custom-toast/custom-toast', () => ({
CustomToast: vi.fn(() => null),
}));

vi.mock('../../utils/show-custom-toast', () => ({
showCustomToast: vi.fn(),
}));

Expand Down
2 changes: 1 addition & 1 deletion src/components/login-form/login-form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {FormEvent, useRef, useCallback, memo} from 'react';
import { useAppDispatch } from '../../hooks';
import { loginAction } from '../../store/api-actions.ts';
import { showCustomToast } from '../custom-toast/custom-toast.tsx';
import {showCustomToast} from '../../utils/show-custom-toast.tsx';

const validatePassword = (password: string): boolean => {
const hasLetter = /[a-zA-Z]/.test(password);
Expand Down
4 changes: 2 additions & 2 deletions src/components/offers-list/offers-list.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ describe('Component: OffersList', () => {
expect(mockSetActiveOfferId).toHaveBeenCalledWith(null);
});

it('should render with "near-places__list places__list" class when isNearby is true', () => {
it('should render with "near-places__list places__list" class when isNearby', () => {
render(
<OffersList
offers={mockOffers}
setActiveOfferId={mockSetActiveOfferId}
isNearby
parentOfferId={'koekfopw-fjioejf382-2'}
/>
);

Expand Down
10 changes: 4 additions & 6 deletions src/components/offers-list/offers-list.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { memo } from 'react';
import { Offer } from '../../types/offer';
import { Card } from '../card/card';

type OffersListProps = {
offers: Offer[];
setActiveOfferId: (id: string | null) => void;
isNearby?: boolean;
parentOfferId?: string;
}

function OffersListComponent({ offers, setActiveOfferId, isNearby = false }: OffersListProps) {
export function OffersList({ offers, setActiveOfferId, parentOfferId = undefined }: OffersListProps) {
const handleMouseEnter = (id: string) => {
setActiveOfferId(id);
};
Expand All @@ -17,7 +16,7 @@ function OffersListComponent({ offers, setActiveOfferId, isNearby = false }: Off
setActiveOfferId(null);
};

const containerName = isNearby ? 'near-places__list places__list' : 'cities__places-list places__list tabs__content';
const containerName = parentOfferId ? 'near-places__list places__list' : 'cities__places-list places__list tabs__content';

return (
<div className={containerName}>
Expand All @@ -27,10 +26,9 @@ function OffersListComponent({ offers, setActiveOfferId, isNearby = false }: Off
key={offer.id}
onMouseEnter={() => handleMouseEnter(offer.id)}
onMouseLeave={handleMouseLeave}
parentOfferId={parentOfferId}
/>
))}
</div>
);
}

export const OffersList = memo(OffersListComponent);
2 changes: 1 addition & 1 deletion src/hooks/useMap/useMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function useMap(mapRef: React.MutableRefObject<null>, city: City) {
}

}
}, [mapRef, city]);
}, [mapRef, city, map]);

return map;
}
Loading

0 comments on commit f2cbe17

Please sign in to comment.