Skip to content

Commit

Permalink
feat: task done
Browse files Browse the repository at this point in the history
  • Loading branch information
PavelLovygin committed Jan 3, 2025
1 parent f7608a7 commit a1bf047
Show file tree
Hide file tree
Showing 13 changed files with 160 additions and 65 deletions.
40 changes: 20 additions & 20 deletions src/components/favorites/favorite-city-section.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
import {GroupBy} from '../../helpers/helpers.ts';
import {OfferCardType} from '../offers/card/offer-card-styles.ts';
import {Offers} from '../../types/offer.ts';
import {OfferCard} from '../offers/card/offer-card.tsx';
import {useAppSelector} from '../../store/hooks.ts';
import {getFavoriteOffers} from '../../store/user/user.selectors.ts';
import {AllCities} from '../../consts.ts';

type FavoritesOfferCardListProps = {
Offers: Offers;
}

export function FavoritesOfferCardList(props: FavoritesOfferCardListProps) {
const cityGroups = GroupBy(props.Offers, 'city');
const citiesSet = new Set(props.Offers.map((offer) => offer.city));
const cities = Array.from(citiesSet.values());
export function FavoritesOfferCardList() {
const favoritesOffers = useAppSelector(getFavoriteOffers);
const cities = AllCities
.filter((city) => favoritesOffers.some((offer) => offer.city.name === city.name));

return (
<ul className="favorites__list">
{cities.map((cityName) => (
<li className="favorites__locations-items" key={cityName.name}>
{cities.map((city) => (
<li className="favorites__locations-items" key={city.name}>
<div className="favorites__locations locations locations--current">
<div className="locations__item">
<a className="locations__item-link" href="#">
<span>{cityName.name}</span>
<span>{city.name}</span>
</a>
</div>
</div>
<div className="favorites__places">
{cityGroups.get(cityName)?.map((city) =>
(
<OfferCard
Offer={{...city}}
OfferCardType={OfferCardType.Favorites}
key={city.id}
/>
))}
{favoritesOffers
.filter((offer) => offer.city.name === city.name)
.map((offer) =>
(
<OfferCard
Offer={{...offer}}
OfferCardType={OfferCardType.Favorites}
key={offer.id}
/>
))}
</div>
</li>
))}
Expand Down
2 changes: 1 addition & 1 deletion src/components/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {FormEvent, useState} from 'react';
import {login} from '../../store/auth-actions.ts';
import {AuthRequest} from '../../types/auth-request.ts';

export function LoginForm(): JSX.Element {
export function LoginForm() {
const dispatch = useAppDispatch();
const [formData, setFormData] = useState<AuthRequest>({
email: '',
Expand Down
7 changes: 4 additions & 3 deletions src/components/nav-bar/nav-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Link, useNavigate} from 'react-router-dom';
import {AppRoute, AuthorizationStatus} from '../../consts.ts';
import {useAppDispatch, useAppSelector} from '../../store/hooks.ts';
import {getAuthorizationStatus, getUserInfo} from '../../store/user/user.selectors.ts';
import {getAuthorizationStatus, getFavoriteOffers, getUserInfo} from '../../store/user/user.selectors.ts';
import {store} from '../../store';
import {checkAuthorizationStatus, logout} from '../../store/auth-actions.ts';

Expand All @@ -12,6 +12,7 @@ export function NavBar(){
const navigate = useNavigate();
const userInfo = useAppSelector(getUserInfo);
const authorizationStatus = useAppSelector(getAuthorizationStatus);
const favoriteOffers = useAppSelector(getFavoriteOffers);

return (
<div className="container">
Expand All @@ -25,11 +26,11 @@ export function NavBar(){
<ul className="header__nav-list">
{authorizationStatus === AuthorizationStatus.Auth &&
<li className="header__nav-item user">
<a className="header__nav-link header__nav-link--profile" href="#">
<a className="header__nav-link header__nav-link--profile">
<div className="header__avatar-wrapper user__avatar-wrapper">
</div>
<span className="header__user-name user__name">{userInfo?.email}</span>
<span className="header__favorite-count">3</span>
<span className="header__favorite-count" onClick={() => navigate(AppRoute.Favorites)}>{favoriteOffers.length}</span>
</a>
</li>}
{authorizationStatus === AuthorizationStatus.Auth &&
Expand Down
22 changes: 22 additions & 0 deletions src/components/offers/card/offer-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import {Link} from 'react-router-dom';
import clsx from 'clsx';
import {offerCardStyles, OfferCardType} from './offer-card-styles.ts';
import {Offer} from '../../../types/offer.ts';
import {useAppDispatch, useAppSelector} from '../../../store/hooks.ts';
import {getAuthorizationStatus} from '../../../store/user/user.selectors.ts';
import {AppRoute, AuthorizationStatus} from '../../../consts.ts';
import {redirectToRoute} from '../../../store/actions.ts';
import {editFavorites} from '../../../store/api-actions.ts';

type OfferCardProps = {
Offer: Offer;
Expand All @@ -11,6 +16,22 @@ type OfferCardProps = {


export function OfferCard(props: OfferCardProps) {

const authorizationStatus = useAppSelector(getAuthorizationStatus);
const dispatch = useAppDispatch();
const handleBookmarkClick = () => {
if (authorizationStatus !== AuthorizationStatus.Auth) {
dispatch(redirectToRoute(AppRoute.Login));
} else {
dispatch(
editFavorites({
offerId: props.Offer.id,
isFavoriteNow: props.Offer.isFavorite,
})
);
}
};

const styles = offerCardStyles[props.OfferCardType];
return (
<article className={`${styles.classPrefix}__card place-card`}>
Expand All @@ -37,6 +58,7 @@ export function OfferCard(props: OfferCardProps) {
<button
className={clsx('place-card__bookmark-button', 'button', props.Offer.isFavorite && 'place-card__bookmark-button--active')}
type="button"
onClick={handleBookmarkClick}
>
<svg className="place-card__bookmark-icon" width={18} height={19}>
<use xlinkHref="#icon-bookmark"/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/review/review-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type ReviewFormProps = {
};


export function ReviewForm(props: ReviewFormProps): JSX.Element {
export function ReviewForm(props: ReviewFormProps) {
const [isFormValid, setIsFormValid] = useState(false);

const [rating, setRating] = useState<number|null>(null);
Expand Down
15 changes: 0 additions & 15 deletions src/helpers/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,3 @@
/**
* Groups all items in an array of objects `T` where the value of property `K` is the same
* @param array Items to group
* @param key Key of `T` to group by
*/
export function GroupBy<T, K extends keyof T>(array: T[], key: K) {
const map = new Map<T[K], T[]>();
array.forEach((item) => {
const itemKey = item[key];
if (!map.has(itemKey)) {
map.set(itemKey, array.filter((i) => i[key] === item[key]));
}
});
return map;
}

export function WordCapitalize(word: string | undefined) {
if (!word) {
Expand Down
11 changes: 6 additions & 5 deletions src/pages/favorites/favorites.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {FavoritesOfferCardList} from '../../components/favorites/favorite-city-section.tsx';
import {Offer} from '../../types/offer.ts';
import {NavBar} from '../../components/nav-bar/nav-bar.tsx';
import {Link} from 'react-router-dom';
import {AppRoute} from '../../consts.ts';

export function Favorites() {
return (
Expand All @@ -11,20 +12,20 @@ export function Favorites() {
<div className="page__favorites-container container">
<section className="favorites">
<h1 className="favorites__title">Saved listing</h1>
<FavoritesOfferCardList Offers={new Array<Offer>()}/>
<FavoritesOfferCardList />
</section>
</div>
</main>
<footer className="footer container">
<a className="footer__logo-link" href="main.html">
<Link className="footer__logo-link" to={AppRoute.Main}>
<img
className="footer__logo"
src="img/logo.svg"
src="public/img/logo.svg"
alt="6 cities logo"
width={64}
height={33}
/>
</a>
</Link>
</footer>
</div>
</>
Expand Down
19 changes: 19 additions & 0 deletions src/pages/main/main-empty.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {useAppSelector} from '../../store/hooks.ts';
import {getCurrentCity} from '../../store/offers/offers.selectors.ts';

export function MainPageEmpty() {
const currentCity = useAppSelector(getCurrentCity);
return (
<div className="cities__places-container cities__places-container--empty container">
<section className="cities__no-places">
<div className="cities__status-wrapper tabs__content">
<b className="cities__status">No places to stay available</b>
<p className="cities__status-description">{`We could not find any property available at the moment in ${currentCity.name}`}</p>
</div>
</section>
<div className="cities__right-section">
<section className="cities__map map"></section>
</div>
</div>
);
}
25 changes: 14 additions & 11 deletions src/pages/main/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {OfferSortCompareFunctions} from '../../components/offers/sort/offer-sort
import {Offer} from '../../types/offer.ts';
import {NavBar} from '../../components/nav-bar/nav-bar.tsx';
import {getCurrentCity, getOffers} from '../../store/offers/offers.selectors.ts';
import {MainPageEmpty} from './main-empty.tsx';

export function Main() {
const [offerSortOptions, setOfferSortOptions] = useState<SortOptions>(SortOptions.Popular);
Expand All @@ -33,17 +34,19 @@ export function Main() {
<CitiesList></CitiesList>
</div>
<div className="cities">
<div className="cities__places-container container">
<section className="cities__places places">
<h2 className="visually-hidden">Places</h2>
<b className="places__found">{sortedOffers.length} places to stay in {currentCity.name}</b>
<OfferSortDropdown SortOption={offerSortOptions} OnSortOptionChange={setOfferSortOptions}/>
<OfferCardList Offers={sortedOffers} OnActiveOfferChange={setActiveOffer}/>
</section>
<div className="cities__right-section">
<Map City={currentCity} Offers={sortedOffers} ActiveOffer={activeOffer} />
</div>
</div>
{sortedOffers.length === 0 ?
<MainPageEmpty/> :
<div className="cities__places-container container">
<section className="cities__places places">
<h2 className="visually-hidden">Places</h2>
<b className="places__found">{sortedOffers.length} places to stay in {currentCity.name}</b>
<OfferSortDropdown SortOption={offerSortOptions} OnSortOptionChange={setOfferSortOptions}/>
<OfferCardList Offers={sortedOffers} OnActiveOfferChange={setActiveOffer}/>
</section>
<div className="cities__right-section">
<Map City={currentCity} Offers={sortedOffers} ActiveOffer={activeOffer}/>
</div>
</div>}
</div>
</main>
</div>
Expand Down
45 changes: 39 additions & 6 deletions src/store/api-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import {AppDispatch} from '../types/state.ts';
import {State} from 'history';
import {AxiosInstance} from 'axios';
import {createAsyncThunk} from '@reduxjs/toolkit';
import {Offers} from '../types/offer.ts';
import {Offer, Offers} from '../types/offer.ts';
import {APIRoutes} from '../consts.ts';
import {setOffers} from './offers/offers.slice.ts';
import {markAsFavorite, setOffers} from './offers/offers.slice.ts';
import {Review, ReviewForm} from '../types/review.ts';
import {setNearbyOffers, setReviews, setSingleOffer} from './single-offer/single-offer.slice.ts';
import {SingleOffer} from '../types/single-offer.ts';
import {setFavoriteOffers, updateUserFavorites} from './user/user.slice.ts';


export const fetchOffers = createAsyncThunk<void, undefined, {
Expand All @@ -30,7 +31,7 @@ export const fetchReviewsAction = createAsyncThunk<
state: State;
extra: AxiosInstance;
}
>('data/fetchReviews', async ({ offerId }, { dispatch, extra: api }) => {
>('fetchReviews', async ({ offerId }, { dispatch, extra: api }) => {
const { data } = await api.get<Review[]>(`${APIRoutes.Comments}/${offerId}`);
dispatch(setReviews({ reviews: data }));
});
Expand All @@ -44,7 +45,7 @@ export const postReviewAction = createAsyncThunk<
extra: AxiosInstance;
}
>(
'review/postReview',
'postReview',
async ({ comment, rating, id }, { dispatch, extra: api }) => {
await api.post(`${APIRoutes.Comments}/${id}`, {
comment,
Expand All @@ -62,7 +63,7 @@ export const fetchNearbyOffersAction = createAsyncThunk<
state: State;
extra: AxiosInstance;
}
>('data/fetchNearbyOffers', async ({ offerId }, { dispatch, extra: api }) => {
>('fetchNearbyOffers', async ({ offerId }, { dispatch, extra: api }) => {
const { data } = await api.get<Offers>(
`${APIRoutes.Offers}/${offerId}/nearby`
);
Expand All @@ -77,9 +78,41 @@ export const fetchSingleOffer = createAsyncThunk<
state: State;
extra: AxiosInstance;
}
>('data/fetchSingleOffer', async ({ offerId }, { dispatch, extra: api }) => {
>('fetchSingleOffer', async ({ offerId }, { dispatch, extra: api }) => {
const { data } = await api.get<SingleOffer>(`${APIRoutes.Offers}/${offerId}`);
dispatch(fetchReviewsAction({ offerId }));
dispatch(fetchNearbyOffersAction({ offerId }));
dispatch(setSingleOffer({ offer: data }));
});

export const fetchFavoriteOffers = createAsyncThunk<
void,
undefined,
{
dispatch: AppDispatch;
state: State;
extra: AxiosInstance;
}
>('data/fetchFavoriteOffers', async (_arg, { dispatch, extra: api }) => {
const { data } = await api.get<Offers>(APIRoutes.Favorites);
dispatch(setFavoriteOffers(data));
});

export const editFavorites = createAsyncThunk<
void,
{ offerId: string; isFavoriteNow: boolean },
{
dispatch: AppDispatch;
state: State;
extra: AxiosInstance;
}
>(
'user/editFavorites',
async ({ offerId, isFavoriteNow }, { dispatch, extra: api }) => {
const { data } = await api.post<Offer & SingleOffer>(
`${APIRoutes.Favorites}/${offerId}/${isFavoriteNow ? 0 : 1}`
);
dispatch(updateUserFavorites({ editedOffer: data }));
dispatch(markAsFavorite(data.id));
}
);
3 changes: 3 additions & 0 deletions src/store/auth-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {dropToken, saveToken} from '../services/token.ts';
import {redirectToRoute} from './actions.ts';
import {setUserInfo} from './user/user.slice.ts';
import {setCity} from './offers/offers.slice.ts';
import {fetchFavoriteOffers} from './api-actions.ts';

export const checkAuthorizationStatus = createAsyncThunk<
void,
Expand All @@ -21,6 +22,7 @@ export const checkAuthorizationStatus = createAsyncThunk<
>('checkAuthorizationStatus', async (_arg, { dispatch, extra: api }) => {
const { data } = await api.get<UserInfo>(APIRoutes.Login);
dispatch(setUserInfo(data));
dispatch(fetchFavoriteOffers());
});

export const login = createAsyncThunk<void, AuthRequest, {
Expand All @@ -35,6 +37,7 @@ export const login = createAsyncThunk<void, AuthRequest, {
dispatch(setUserInfo(data));
dispatch(redirectToRoute(AppRoute.Main));
dispatch(checkAuthorizationStatus());
dispatch(fetchFavoriteOffers());
}
);

Expand Down
14 changes: 12 additions & 2 deletions src/store/offers/offers.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,17 @@ export const dataProcess = createSlice({
},
setOffers: (state, action: PayloadAction<Offers>) => {
state.Offers = action.payload;
}
},
markAsFavorite(state, action: PayloadAction<string>) {
const id = action.payload;
const offerIndex = state.Offers.findIndex(
(offer) => offer.id === id
);
if (offerIndex !== -1) {
state.Offers[offerIndex].isFavorite = !state.Offers[offerIndex].isFavorite;
}
},

},
extraReducers(builder) {
builder
Expand All @@ -37,4 +47,4 @@ export const dataProcess = createSlice({
},
});

export const { setCity, setOffers } = dataProcess.actions;
export const { setCity, setOffers, markAsFavorite } = dataProcess.actions;
Loading

0 comments on commit a1bf047

Please sign in to comment.