Skip to content

Commit

Permalink
форма и фавориты
Browse files Browse the repository at this point in the history
  • Loading branch information
denispan committed Apr 8, 2024
1 parent a70bbaf commit cbfc32d
Show file tree
Hide file tree
Showing 15 changed files with 151 additions and 69 deletions.
4 changes: 3 additions & 1 deletion src/components/comments-list/comments-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ interface CommentsListProps {
offerId: OfferFullInfo['id'];
}

const MAX_COUNT_COMMENTS = 10;

function CommentsList ({offerId}: CommentsListProps) {
const {fetchComments} = useActionCreators(commentsActions);
const postCommentStatus = useAppSelector(commentsSelectors.statusPostRequest);
const comments = useAppSelector(commentsSelectors.comments);
const comments = useAppSelector(commentsSelectors.sortedComments).slice(0, MAX_COUNT_COMMENTS);
const commentsCount = comments.length;

useEffect(() => {
Expand Down
26 changes: 20 additions & 6 deletions src/components/favorite-button/favorite-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {OfferShortInfo} from '../../types/offer.ts';
import {useActionCreators, useAppSelector} from '../../hooks/store.ts';
import {favoritesActions, favoritesSelectors} from '../../store/slices/favorites.ts';
import {useState} from 'react';
import {RequestStatus} from '../../const.ts';
import {AppRoute, AuthStatus, RequestStatus} from '../../const.ts';
import {toast} from 'react-toastify';
import {useNavigate} from 'react-router-dom';
import {userSelectors} from '../../store/slices/user.ts';

interface FavoriteButtonProps {
componentType: 'place-card' | 'offer';
Expand All @@ -27,12 +29,24 @@ function FavoriteButton({componentType, isFavorite, offerId}: FavoriteButtonProp
const [isFavoriteCurrent, setIsFavoriteCurrent] = useState(isFavorite);
const {toggleFavorite} = useActionCreators(favoritesActions);
const statusToggleFavorite = useAppSelector(favoritesSelectors.statusToggleFavorite);
const authStatus = useAppSelector(userSelectors.authStatus);
const navigate = useNavigate();

const isAuth = authStatus === AuthStatus.Auth;

const onClickHandler = () => {
toast.promise(toggleFavorite({status: Number(!isFavoriteCurrent) as 0 | 1, offerId})
.unwrap()
.then(() => setIsFavoriteCurrent(!isFavoriteCurrent)), {
pending: 'Запрос отправляется',
if (!isAuth) {
navigate(AppRoute.Login);
}

toast.promise(toggleFavorite({status: Number(!isFavoriteCurrent) as 0 | 1, offerId}).unwrap(), {
pending: 'Sending request',
success: {
render() {
setIsFavoriteCurrent(!isFavoriteCurrent);
return 'Success';
}
},
});
};

Expand All @@ -41,7 +55,7 @@ function FavoriteButton({componentType, isFavorite, offerId}: FavoriteButtonProp
className={
classNames(
`${componentType}__bookmark-button button`,
isFavoriteCurrent && `${componentType}__bookmark-button--active`)
isFavoriteCurrent && isAuth && `${componentType}__bookmark-button--active`)
}
type="button"
onClick={onClickHandler}
Expand Down
5 changes: 5 additions & 0 deletions src/components/form-review/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const MIN_LENGTH_COMMENT = 50;
const MAX_LENGTH_COMMENT = 300;
const MIN_STARS_COMMENT = 1;

export {MAX_LENGTH_COMMENT, MIN_STARS_COMMENT, MIN_LENGTH_COMMENT};
80 changes: 54 additions & 26 deletions src/components/form-review/form-review.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,75 @@
import {MIN_LENGTH_COMMENT, MIN_STARS_COMMENT, RATING_STARS} from '../../const.ts';
import {StarTitle, StarValue} from '../rating-star/rating-star.tsx';
import RatingStar from '../rating-star/rating-star.tsx';
import {FormEvent, useRef, useState} from 'react';
import {RATING_STARS} from '../../const.ts';
import RatingStar, {StarTitle} from '../rating-star/rating-star.tsx';
import {FormEvent, useState} from 'react';
import {useActionCreators} from '../../hooks/store.ts';
import {commentsActions} from '../../store/slices/comments.ts';
import {OfferFullInfo} from '../../types/offer.ts';
import {MAX_LENGTH_COMMENT, MIN_LENGTH_COMMENT} from './const.ts';
import {toast} from 'react-toastify';

type FormReviewProps = {
offerId: OfferFullInfo['id'];
};

type InitialForm = {
comment: string;
rating: StarValue;
type Form = HTMLFormElement & {
rating: RadioNodeList;
review: HTMLTextAreaElement;
};

const INITIAL_FORM_VALUE: InitialForm = {
comment: '',
rating: RATING_STARS['unknown'],
};
const verifyForm = (comment: string, rating: string) =>
comment.length <= MIN_LENGTH_COMMENT || comment.length > MAX_LENGTH_COMMENT || Number(rating) === RATING_STARS.unknown;

function FormReview({offerId}: FormReviewProps) {
const [textAreaValue, setTextAreaValue] = useState(INITIAL_FORM_VALUE.comment);
const [ratingStar, setRatingStar] = useState(INITIAL_FORM_VALUE.rating);
const [isSubmitDisabled, setIsSubmitDisabled] = useState(true);
const [isFormDisabled, setIsFormDisabled] = useState(false);
const {postComment} = useActionCreators(commentsActions);
const formRef = useRef(null);

const onChangeForm = (evt: FormEvent<HTMLFormElement>) => {
const form = evt.currentTarget as Form;
const rating = form.rating.value;
const comment = form.review.value;
setIsSubmitDisabled(verifyForm(comment, rating));
};

const onFormSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
postComment({offerId, body: {comment: textAreaValue, rating: ratingStar}})
.unwrap()
.then(() => {
setTextAreaValue(INITIAL_FORM_VALUE.comment);
setRatingStar(INITIAL_FORM_VALUE.rating);
});
const form = evt.currentTarget as Form;

const clearForm = () => {
form.reset();
setIsSubmitDisabled(true);
setIsFormDisabled(false);
};

const commentToSend = {
offerId,
body: {
comment: form.review.value,
rating: Number(form.rating.value),
},
};
setIsFormDisabled(true);
toast.promise(postComment(commentToSend).unwrap(), {
pending: 'Sending comment',
success: {
render() {
clearForm();
return 'Comment sent';
}
},
error: {
render() {
setIsFormDisabled(false);
return 'Failed to send comment';
}
}
});
};

return (
<form
ref={formRef}
onSubmit={(evt) => onFormSubmit(evt)}
onChange={(evt) => onChangeForm(evt)}
className="reviews__form form"
action="#"
method="post"
Expand All @@ -49,21 +79,19 @@ function FormReview({offerId}: FormReviewProps) {
{Object.entries(RATING_STARS).slice(1).map(([starTitle, starValue]) =>
(
<RatingStar
onClickHandle={setRatingStar}
key={starTitle}
starTitle={starTitle as StarTitle}
starValue={starValue}
currentValue={ratingStar}
isDisabled={isFormDisabled}
/>
)
)}
</div>
<textarea
onChange={(evt) => setTextAreaValue(evt.target.value)}
value={textAreaValue}
className="reviews__textarea form__textarea"
id="review" name="review"
placeholder="Tell how was your stay, what you like and what can be improved"
disabled={isFormDisabled}
/>
<div className="reviews__button-wrapper">
<p className="reviews__help">
Expand All @@ -73,7 +101,7 @@ function FormReview({offerId}: FormReviewProps) {
<button
className="reviews__submit form__submit button"
type="submit"
disabled={textAreaValue.length < MIN_LENGTH_COMMENT || ratingStar < MIN_STARS_COMMENT}
disabled={isSubmitDisabled || isFormDisabled}
>
Submit
</button>
Expand Down
2 changes: 1 addition & 1 deletion src/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function Header() {
<div className="header__avatar-wrapper user__avatar-wrapper">
{userInfo?.avatarUrl && <img className="user__avatar" src={userInfo.avatarUrl} alt="avatar"/>}
</div>
{userInfo?.name && <span className="header__user-name user__name">{userInfo.name}</span>}
{userInfo?.email && <span className="header__user-name user__name">{userInfo.email}</span>}
<span className="header__favorite-count">{favoritesCount}</span>
</Link>
</li>
Expand Down
15 changes: 12 additions & 3 deletions src/components/map/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ import {Icon, layerGroup, Marker} from 'leaflet';
import {CITIES, MARKER_ACTIVE_OPTIONS, MARKER_DEFAULT_OPTIONS} from '../../const';
import 'leaflet/dist/leaflet.css';
import useMap from '../../hooks/use-map.tsx';
import {OfferShortInfo} from '../../types/offer.ts';
import {OfferFullInfo, OfferShortInfo} from '../../types/offer.ts';
import {useAppSelector} from '../../hooks/store.ts';
import {offersSelectors} from '../../store/slices/offers.ts';

type MapProps = {
city: typeof CITIES[number];
offers: OfferShortInfo[];
container: string;
currentOffer?: OfferFullInfo;
};

const defaultCustomIcon = new Icon(MARKER_DEFAULT_OPTIONS);

const currentCustomIcon = new Icon(MARKER_ACTIVE_OPTIONS);

function Map({container, city, offers}: MapProps) {
function Map({container, city, offers, currentOffer}: MapProps) {
const activeOffer = useAppSelector(offersSelectors.activeOffer);

const mapRef = useRef(null);
Expand All @@ -40,6 +40,15 @@ function Map({container, city, offers}: MapProps) {
: defaultCustomIcon
)
.addTo(markerLayer);

if (currentOffer) {
const currentOfferMarker = new Marker({
lat: currentOffer.location.latitude,
lng: currentOffer.location.longitude
});

currentOfferMarker.setIcon(currentCustomIcon).addTo(map);
}
});

map.flyTo([city.location.latitude, city.location.longitude], 12);
Expand Down
17 changes: 15 additions & 2 deletions src/components/offers-list/offers-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,27 @@ function OffersList ({offersByCity, city}: OffersListProps) {
const {setActiveOffer} = useActionCreators(offersActions);
const status = useAppSelector(offersSelectors.status);

const offersSorted = sortOffers(activeSortOption, offersByCity);

if (status === RequestStatus.Loading) {
return (
<OffersListLoader />
);
}

if (offersByCity.length === 0) {
return (
<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 {city.name}
</p>
</div>
</section>
);
}

const offersSorted = sortOffers(activeSortOption, offersByCity);

return (
<section className="cities__places places">
<h2 className="visually-hidden">Places</h2>
Expand Down
8 changes: 3 additions & 5 deletions src/components/rating-star/rating-star.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ export type StarValue = (typeof RATING_STARS)[keyof typeof RATING_STARS]
interface RatingStar {
starTitle: StarTitle;
starValue: StarValue;
currentValue: StarValue;
onClickHandle: React.Dispatch<React.SetStateAction<StarValue>>;
isDisabled?: boolean;
}

function RatingStar({starTitle, starValue, onClickHandle, currentValue}: RatingStar) {
function RatingStar({starTitle, starValue, isDisabled}: RatingStar) {
return (
<>
<input
Expand All @@ -19,10 +18,9 @@ function RatingStar({starTitle, starValue, onClickHandle, currentValue}: RatingS
value={starValue}
id={`${starValue}-stars`}
type="radio"
checked={currentValue === starValue}
disabled={isDisabled}
/>
<label
onClick={() => onClickHandle(starValue)}
htmlFor={`${starValue}-stars`}
className="reviews__rating-label form__rating-label"
title={starTitle}
Expand Down
5 changes: 3 additions & 2 deletions src/components/rating/rating.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import {OfferShortInfo} from '../../types/offer.ts';
import {getRatingValue} from '../../utils/common.ts';

interface FavoriteButtonProps {
interface RatingProps {
componentType: 'place-card' | 'offer' | 'reviews';
rating: OfferShortInfo['rating'];
}

function Rating({componentType, rating}: FavoriteButtonProps) {
function Rating({componentType, rating}: RatingProps) {

return (
<div className={`${componentType}__rating rating`}>
<div className={`${componentType}__stars rating__stars`}>
<span style={{width: `${getRatingValue(rating)}%`,}}></span>
<span className="visually-hidden">Rating</span>
</div>
{componentType === 'offer' && <span className="offer__rating-value rating__value">{rating}</span>}
</div>
);
}
Expand Down
5 changes: 0 additions & 5 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ const RATING_STARS = {
'terribly': 1,
} as const;

const MIN_LENGTH_COMMENT = 50;
const MIN_STARS_COMMENT = 1;

const CITIES = [
{name: 'Paris', location: {latitude: 48.85661, longitude: 2.351499, zoom: 13}, slug: 'paris'},
{name: 'Cologne', location: {latitude: 50.938361, longitude: 6.959974, zoom: 13}, slug: 'cologne'},
Expand Down Expand Up @@ -94,8 +91,6 @@ export {
DEFAULT_CITY_SLUG,
DATE_FORMAT,
DATE_FORMAT_ATTRIBUTE,
MIN_STARS_COMMENT,
MIN_LENGTH_COMMENT,
MARKER_DEFAULT_OPTIONS,
MARKER_ACTIVE_OPTIONS,
RequestStatus,
Expand Down
8 changes: 5 additions & 3 deletions src/pages/main/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {useEffect} from 'react';
import OffersList from '../../components/offers-list/offers-list.tsx';
import {toast} from 'react-toastify';
import {AxiosError} from 'axios';
import {classNames} from '../../utils/class-names/class-names.ts';

export type MainProps = {
title?: string;
Expand Down Expand Up @@ -40,8 +41,9 @@ function Main({title = 'Main', citySlug}: MainProps) {
}

const offersByCity = allOffers.filter((offer) => offer.city.name === activeCity.name);

return (
<div className="page page--gray page--main">
<div className={classNames('page page--gray page--main', offersByCity.length === 0 && 'page__main--index-empty')}>
<Header />
<main className="page__main page__main--index">
<h1 className="visually-hidden">Cities</h1>
Expand All @@ -53,11 +55,11 @@ function Main({title = 'Main', citySlug}: MainProps) {
</section>
</div>
<div className="cities">
<div className="cities__places-container container">
<div className={classNames('cities__places-container container', offersByCity.length === 0 && 'cities__places-container--empty')}>
<OffersList offersByCity={offersByCity} city={activeCity} />
<div className="cities__right-section">
{
activeCity && <Map container="cities" city={activeCity} offers={offersByCity} />
activeCity && offersByCity.length > 0 && <Map container="cities" city={activeCity} offers={offersByCity} />
}
</div>
</div>
Expand Down
Loading

0 comments on commit cbfc32d

Please sign in to comment.