Skip to content

Commit

Permalink
Merge pull request #15 from PavelLovygin/module9-task1
Browse files Browse the repository at this point in the history
  • Loading branch information
keksobot authored Jan 4, 2025
2 parents f9a1481 + 84c529e commit a4b7e61
Show file tree
Hide file tree
Showing 14 changed files with 176 additions and 88 deletions.
16 changes: 16 additions & 0 deletions src/components/bookmarks/bookmark-styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

export enum BookmarkStyle {
Offer,
OfferCard,
}

type BookmarkStyleFields = {
width: number;
height: number;
classPrefix: string;
}

export const bookmarkStyles : Record<BookmarkStyle, BookmarkStyleFields> = {
[BookmarkStyle.OfferCard]: {width: 18, height: 19, classPrefix: 'place-card'},
[BookmarkStyle.Offer]: {width: 31, height: 33, classPrefix: 'offer'},
};
50 changes: 50 additions & 0 deletions src/components/bookmarks/bookmark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import clsx from 'clsx';
import {useAppDispatch, useAppSelector} from '../../store/hooks.ts';
import {getAuthorizationStatus} from '../../store/user/user.selectors.ts';
import {redirectToRoute} from '../../store/actions.ts';
import {AppRoute, AuthorizationStatus} from '../../consts.ts';
import {editFavorites} from '../../store/api-actions.ts';
import {BookmarkStyle, bookmarkStyles} from './bookmark-styles.ts';
import {Offer} from '../../types/offer.ts';
import {SingleOffer} from '../../types/single-offer.ts';

type BookmarkProps = {
Offer: Offer | SingleOffer;
BookmarkStyle: BookmarkStyle;
}

export function Bookmark(props: BookmarkProps) {
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 style = bookmarkStyles[props.BookmarkStyle];

return (
<button
className={clsx(
`${style.classPrefix}__bookmark-button`,
'button',
props.Offer.isFavorite && `${style.classPrefix}__bookmark-button--active`)}
type="button"
onClick={handleBookmarkClick}
>
<svg className={`${style.classPrefix}__bookmark-icon`} width={style.width} height={style.height}>
<use xlinkHref="#icon-bookmark"/>
</svg>
<span className="visually-hidden">To bookmarks</span>
</button>
);
}
2 changes: 1 addition & 1 deletion src/components/nav-bar/nav-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function NavBar(){
<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__user-name user__name" onClick={() => navigate(AppRoute.Favorites)}>{userInfo?.email}</span>
<span className="header__favorite-count" onClick={() => navigate(AppRoute.Favorites)}>{favoriteOffers.length}</span>
</a>
</li>}
Expand Down
39 changes: 6 additions & 33 deletions src/components/offers/card/offer-card.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import {Premium} from './premium.tsx';
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';
import {Bookmark} from '../../bookmarks/bookmark.tsx';
import {BookmarkStyle} from '../../bookmarks/bookmark-styles.ts';

type OfferCardProps = {
Offer: Offer;
Expand All @@ -17,22 +13,8 @@ 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`}>
{props.Offer.isPremium ? <Premium/> : null}
Expand All @@ -55,25 +37,16 @@ export function OfferCard(props: OfferCardProps) {
/&nbsp;night
</span>
</div>
<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"/>
</svg>
<span className="visually-hidden">In bookmarks</span>
</button>
<Bookmark Offer={props.Offer} BookmarkStyle={BookmarkStyle.OfferCard} />
</div>
<div className="place-card__rating rating">
<div className="place-card__stars rating__stars">
<span style={{width: `${props.Offer.rating * 20}%`}}/>
<span style={{width: `${Math.round(props.Offer.rating) * 20}%`}}/>
<span className="visually-hidden">Rating</span>
</div>
</div>
<h2 className="place-card__name" >
<Link to={`offer/${props.Offer.id}`}>{props.Offer.title}</Link>
<Link to={`/offer/${props.Offer.id}`}>{props.Offer.title}</Link>
</h2>
<p className="place-card__type">{props.Offer.type}</p>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/components/offers/rating/rating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function Rating(props: RatingProps) {
onChange={() => {
props.SetRating(rating.value);
}}
value={rating.value}
/>
<label
htmlFor={`${rating.value}-stars`}
Expand Down
21 changes: 18 additions & 3 deletions src/components/review/review-form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {FormEvent, useEffect, useState} from 'react';
import {Rating} from '../offers/rating/rating.tsx';
import {postReviewAction} from '../../store/api-actions.ts';
import {useAppDispatch} from '../../store/hooks.ts';
import {useAppDispatch, useAppSelector} from '../../store/hooks.ts';
import {getReviewStatus} from '../../store/single-offer/single-offer.selectors.ts';
import {ReviewStatus} from '../../types/review-status.ts';
import {setReviewStatus} from '../../store/single-offer/single-offer.slice.ts';

type ReviewFormProps = {
OfferId: string;
Expand All @@ -10,15 +13,16 @@ type ReviewFormProps = {

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

const [rating, setRating] = useState<number|null>(null);
const [comment, setComment] = useState<string>('');

const reviewStatus = useAppSelector(getReviewStatus);
const dispatch = useAppDispatch();

useEffect(() => {
if (
comment.length > 50 &&
comment.length < 300 &&
rating !== null
) {
setIsFormValid(true);
Expand All @@ -27,6 +31,7 @@ export function ReviewForm(props: ReviewFormProps) {
}
}, [rating, comment]);


const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
dispatch(
Expand All @@ -36,7 +41,16 @@ export function ReviewForm(props: ReviewFormProps) {
id: props.OfferId,
})
);

};
useEffect(() => {
if (reviewStatus === ReviewStatus.Posted) {
setComment('');
setRating(null);

dispatch(setReviewStatus(ReviewStatus.None));
}
}, [dispatch, reviewStatus]);


return (
Expand All @@ -54,6 +68,7 @@ export function ReviewForm(props: ReviewFormProps) {
onChange={({ target: { value } }) => {
setComment(value);
}}
value={comment}
/>
<div className="reviews__button-wrapper">
<p className="reviews__help">
Expand All @@ -65,7 +80,7 @@ export function ReviewForm(props: ReviewFormProps) {
<button
className="reviews__submit form__submit button"
type="submit"
disabled={!isFormValid}
disabled={!isFormValid || reviewStatus === ReviewStatus.PostPending}
>
Submit
</button>
Expand Down
22 changes: 18 additions & 4 deletions src/pages/favorites/favorites.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,32 @@ import {FavoritesOfferCardList} from '../../components/favorites/favorite-city-s
import {NavBar} from '../../components/nav-bar/nav-bar.tsx';
import {Link} from 'react-router-dom';
import {AppRoute} from '../../consts.ts';
import {useAppSelector} from '../../store/hooks.ts';
import {getFavoriteOffers} from '../../store/user/user.selectors.ts';

export function Favorites() {
const favoritesOffers = useAppSelector(getFavoriteOffers);

return (
<>
<NavBar/>
<div className="page">
<main className="page__main page__main--favorites">
<div className="page__favorites-container container">
<section className="favorites">
<h1 className="favorites__title">Saved listing</h1>
<FavoritesOfferCardList />
</section>
{favoritesOffers.length === 0 ?
<section className="favorites favorites--empty">
<h1 className="visually-hidden">Favorites (empty)</h1>
<div className="favorites__status-wrapper">
<b className="favorites__status">Nothing yet saved.</b>
<p className="favorites__status-description">Save properties to narrow down search or plan your future
trips.
</p>
</div>
</section> :
<section className="favorites">
<h1 className="favorites__title">Saved listing</h1>
<FavoritesOfferCardList/>
</section>}
</div>
</main>
<footer className="footer container">
Expand Down
12 changes: 11 additions & 1 deletion src/pages/login/login.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import {Helmet} from 'react-helmet-async';
import {AppName} from '../../consts.ts';
import {AppName, AppRoute, AuthorizationStatus} from '../../consts.ts';
import {LoginForm} from '../../components/login/login-form.tsx';
import {useAppDispatch, useAppSelector} from '../../store/hooks.ts';
import { getAuthorizationStatus } from '../../store/user/user.selectors.ts';
import {redirectToRoute} from '../../store/actions.ts';

export function Login() {
const dispatch = useAppDispatch();
const authorizationStatus = useAppSelector(getAuthorizationStatus);

if (authorizationStatus === AuthorizationStatus.Auth) {
dispatch(redirectToRoute(AppRoute.Main));
}

return (
<div className="page page--gray page--login">
<Helmet>
Expand Down
31 changes: 14 additions & 17 deletions src/pages/offer/offer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import {
import clsx from 'clsx';
import {WordCapitalize} from '../../helpers/helpers.ts';
import {Reviews} from '../../components/review/reviews.tsx';
import {getAuthorizationStatus, getUserInfo} from '../../store/user/user.selectors.ts';
import {getAuthorizationStatus} from '../../store/user/user.selectors.ts';
import {OfferCard} from '../../components/offers/card/offer-card.tsx';
import {OfferCardType} from '../../components/offers/card/offer-card-styles.ts';
import {NotFound} from '../not-found/not-found.tsx';
import {Loading} from '../../components/loading/Loading.tsx';
import {NavBar} from '../../components/nav-bar/nav-bar.tsx';
import {Bookmark} from '../../components/bookmarks/bookmark.tsx';
import {BookmarkStyle} from '../../components/bookmarks/bookmark-styles.ts';

export function Offer() {
const offerId = useParams<{ id: string }>().id;
Expand All @@ -32,12 +34,10 @@ export function Offer() {
}
}, [offerId, dispatch]);

const userInfo = useAppSelector(getUserInfo);
const offer = useAppSelector(getSingleOffer);
const isLoaded = useAppSelector(getSingleOfferDataLoadingStatus);
const reviews = useAppSelector(getReviews);
const authorizationStatus = useAppSelector(getAuthorizationStatus);
const isOfferPosted = reviews.some((review) => review.user.name === userInfo?.name);
const nearbyOffers = useAppSelector(getNearbyOffers);

if (!isLoaded) {
Expand Down Expand Up @@ -80,19 +80,11 @@ export function Offer() {
<h1 className="offer__name">
{offer.title}
</h1>
<button
className={clsx('offer__bookmark-button', 'button', offer.isFavorite && 'offer__bookmark-button--active')}
type="button"
>
<svg className="offer__bookmark-icon" width={31} height={33}>
<use xlinkHref="#icon-bookmark"/>
</svg>
<span className="visually-hidden">To bookmarks</span>
</button>
<Bookmark Offer={offer} BookmarkStyle={BookmarkStyle.Offer} />
</div>
<div className="offer__rating rating">
<div className="offer__stars rating__stars">
<span style={{width: `${((offer.rating ?? 0) * 20)}%`}}>
<span style={{width: `${(Math.round(offer.rating) * 20)}%`}}>
<span className="visually-hidden">Rating</span>
</span>
</div>
Expand All @@ -101,10 +93,10 @@ export function Offer() {
<ul className="offer__features">
<li className="offer__feature offer__feature--entire">{WordCapitalize(offer.type)}</li>
<li className="offer__feature offer__feature--bedrooms">
{offer.bedrooms} Bedrooms
{offer.bedrooms} {offer.bedrooms === 1 ? 'Bedroom' : 'Bedrooms' }
</li>
<li className="offer__feature offer__feature--adults">
Max {offer.maxAdults} adults
Max {offer.maxAdults} {offer.maxAdults === 1 ? 'adult' : 'adults' }
</li>
</ul>
<div className="offer__price">
Expand Down Expand Up @@ -146,8 +138,13 @@ export function Offer() {
<h2 className="reviews__title">
Reviews · <span className="reviews__amount">{reviews.length}</span>
</h2>
<Reviews Reviews={reviews}></Reviews>
{authorizationStatus === AuthorizationStatus.Auth && !isOfferPosted && <ReviewForm OfferId={offer.id ?? ''}/>}
<Reviews Reviews={
reviews
.toSorted((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.slice(0, 10)
}
/>
{authorizationStatus === AuthorizationStatus.Auth && <ReviewForm OfferId={offer.id ?? ''}/>}
</section>
</div>
</div>
Expand Down
13 changes: 10 additions & 3 deletions src/store/api-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import {Offer, Offers} from '../types/offer.ts';
import {APIRoutes} from '../consts.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 {
addReview,
setNearbyOffers,
setReviews,
setSingleOffer,
updateSingleOfferFavoritesStatus
} from './single-offer/single-offer.slice.ts';
import {SingleOffer} from '../types/single-offer.ts';
import {setFavoriteOffers, updateUserFavorites} from './user/user.slice.ts';

Expand Down Expand Up @@ -47,11 +53,11 @@ export const postReviewAction = createAsyncThunk<
>(
'postReview',
async ({ comment, rating, id }, { dispatch, extra: api }) => {
await api.post(`${APIRoutes.Comments}/${id}`, {
const { data } = await api.post<Review>(`${APIRoutes.Comments}/${id}`, {
comment,
rating: Number(rating),
});
dispatch(fetchReviewsAction({ offerId: id }));
dispatch(addReview(data));
}
);

Expand Down Expand Up @@ -113,6 +119,7 @@ export const editFavorites = createAsyncThunk<
`${APIRoutes.Favorites}/${offerId}/${isFavoriteNow ? 0 : 1}`
);
dispatch(updateUserFavorites({ editedOffer: data }));
dispatch(updateSingleOfferFavoritesStatus());
dispatch(markAsFavorite(data.id));
}
);
Loading

0 comments on commit a4b7e61

Please sign in to comment.