Skip to content

Commit

Permalink
Добро пожаловать, или посторонним вход воспрещён (часть 2) (#11)
Browse files Browse the repository at this point in the history
* добавляет работу формы для комментариев
* добавляет работу формы для комментариев
* favorites
* роуты
* форма и фавориты
  • Loading branch information
denispan authored Apr 8, 2024
1 parent c42a674 commit 5a37f2f
Show file tree
Hide file tree
Showing 42 changed files with 613 additions and 201 deletions.
26 changes: 4 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"history": "5.3.0",
"http-status-codes": "2.3.0",
"leaflet": "1.7.1",
"nanoid": "5.0.3",
"react": "18.2.0",
"react-content-loader": "6.0.3",
"react-dom": "18.2.0",
Expand Down Expand Up @@ -46,7 +45,7 @@
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-refresh": "0.4.3",
"jsdom": "22.1.0",
"typescript": "5.2.2",
"typescript": "5.4.2",
"vite": "4.4.11",
"vitest": "0.34.6"
},
Expand Down
15 changes: 3 additions & 12 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import Main from './pages/main/main.tsx';
import {BrowserRouter, Navigate, Route, Routes} from 'react-router-dom';
import NotFound from './pages/not-found/not-found.tsx';
import {AppRoute, AuthStatus, CITIES, DEFAULT_CITY_SLUG} from './const.ts';
import {AppRoute, CITIES, DEFAULT_CITY_SLUG} from './const.ts';
import Favorites from './pages/favorites/favorites.tsx';
import Offer from './pages/offer/offer.tsx';
import PrivateRoute from './components/private-route/private-route.tsx';
import Login from './pages/login/login.tsx';
import PublicRoute from './components/public-route/public-route.tsx';
import {useActionCreators, useAppSelector} from './hooks/store.ts';
import {userActions, userSelectors} from './store/slices/user.ts';
import {useEffect} from 'react';
import {PrivateRoute, PublicRoute} from './components/access-route/access-route.tsx';

function App() {
const {checkAuth} = useActionCreators(userActions);
const authStatus = useAppSelector(userSelectors.authStatus);
useEffect(() => {
checkAuth();
}, [authStatus, checkAuth]);

return (
<BrowserRouter>
Expand Down Expand Up @@ -51,7 +42,7 @@ function App() {
/>
<Route
path={`${AppRoute.Offer}/:offerId`}
element={<Offer userAuth={AuthStatus.Auth} />}
element={<Offer />}
/>
<Route
path="/*"
Expand Down
44 changes: 44 additions & 0 deletions src/components/access-route/access-route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {AppRoute, AuthStatus} from '../../const.ts';
import {Navigate, useLocation} from 'react-router-dom';
import React from 'react';
import {useAppSelector} from '../../hooks/store.ts';
import {userSelectors} from '../../store/slices/user.ts';
import Loader from '../loader/loader.tsx';

type AccessRouteProps = {
children: React.JSX.Element;
}

type Redirect = {
from?: string;
};

const createAccessRoute = (status: AuthStatus, fallback: AppRoute) =>
function AccessRoute({children}: AccessRouteProps) {
const authStatus = useAppSelector(userSelectors.authStatus);
const location = useLocation();

if (authStatus === AuthStatus.Unknown) {
return <Loader/>;
}

const redirect = (location.state as Redirect ?? {}).from ?? fallback;

return (
authStatus === status
? children
:
<Navigate
to={redirect}
state={{
from: location.pathname
}}
/>
);
};

const PrivateRoute = createAccessRoute(AuthStatus.Auth, AppRoute.Login);
const PublicRoute = createAccessRoute(AuthStatus.NoAuth, AppRoute.Root);


export {PrivateRoute, PublicRoute};
39 changes: 39 additions & 0 deletions src/components/comments-list/comments-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type {OfferFullInfo} from '../../types/offer.ts';
import {useActionCreators, useAppSelector} from '../../hooks/store.ts';
import {useEffect} from 'react';
import {commentsActions, commentsSelectors} from '../../store/slices/comments.ts';
import Review from '../review/review.tsx';

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.sortedComments).slice(0, MAX_COUNT_COMMENTS);
const commentsCount = comments.length;

useEffect(() => {
fetchComments(offerId);
}, [postCommentStatus, offerId]);

Check warning on line 21 in src/components/comments-list/comments-list.tsx

View workflow job for this annotation

GitHub Actions / Check

React Hook useEffect has a missing dependency: 'fetchComments'. Either include it or remove the dependency array

return (
<>
<h2 className="reviews__title">
Reviews &middot; <span className="reviews__amount">{commentsCount}</span>
</h2>
<ul className="reviews__list">
{
commentsCount > 0 &&
comments.map((comment) => <Review key={comment.id} review={comment}/>
)
}
</ul>
</>
);
}

export default CommentsList;
45 changes: 41 additions & 4 deletions src/components/favorite-button/favorite-button.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import {classNames} from '../../utils/class-names/class-names.ts';
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 {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';
isFavorite: OfferShortInfo['isFavorite'];
offerId: OfferShortInfo['id'];
}

function FavoriteButton({componentType, isFavorite}: FavoriteButtonProps) {
function FavoriteButton({componentType, isFavorite, offerId}: FavoriteButtonProps) {
const sizes = {
'place-card': {
width: '18',
Expand All @@ -18,17 +26,46 @@ function FavoriteButton({componentType, isFavorite}: FavoriteButtonProps) {
},
} as const;

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 = () => {
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';
}
},
});
};

return (
<button
className={classNames(`
${componentType}__bookmark-button button`, isFavorite && `${componentType}__bookmark-button--active`)}
className={
classNames(
`${componentType}__bookmark-button button`,
isFavoriteCurrent && isAuth && `${componentType}__bookmark-button--active`)
}
type="button"
onClick={onClickHandler}
disabled={statusToggleFavorite === RequestStatus.Loading}
>
<svg className={`${componentType}__bookmark-icon`} {...sizes[componentType]}>
<use xlinkHref="#icon-bookmark"></use>
</svg>
<span className="visually-hidden">
{isFavorite ? 'In bookmarks' : 'To bookmarks'}
{isFavoriteCurrent ? 'In bookmarks' : 'To bookmarks'}
</span>
</button>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ function LoginForm() {
e.preventDefault();
toast.promise(login({email, password}).unwrap(), {
pending: 'Loading',
success: 'Success',
error: 'Error',
});
};

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};
76 changes: 66 additions & 10 deletions src/components/form-review/form-review.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,97 @@
import {MIN_LENGTH_COMMENT, MIN_STARS_COMMENT, RATING_STARS} from '../../const.ts';
import RatingStar from '../rating-star/rating-star.tsx';
import type {StarTitle} from '../rating-star/rating-star.tsx';
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';

function FormReview() {
const [ratingStar, setRatingStar] = useState(0);
const [textAreaValue, setTextAreaValue] = useState('');
type FormReviewProps = {
offerId: OfferFullInfo['id'];
};

type Form = HTMLFormElement & {
rating: RadioNodeList;
review: HTMLTextAreaElement;
};

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 [isSubmitDisabled, setIsSubmitDisabled] = useState(true);
const [isFormDisabled, setIsFormDisabled] = useState(false);
const {postComment} = useActionCreators(commentsActions);

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();
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
onSubmit={(evt) => onFormSubmit(evt)}
onChange={(evt) => onChangeForm(evt)}
className="reviews__form form"
action="#"
method="post"
>
<label className="reviews__label form__label" htmlFor="review">Your review</label>
<div className="reviews__rating-form form__rating">
{Object.entries(RATING_STARS).map(([starTitle, starValue]) =>
{Object.entries(RATING_STARS).slice(1).map(([starTitle, starValue]) =>
(
<RatingStar
onClickHandle={setRatingStar}
key={starTitle}
starTitle={starTitle as StarTitle}
starValue={starValue}
isDisabled={isFormDisabled}
/>
)
)}
</div>
<textarea
onChange={(evt) => setTextAreaValue(evt.target.value)}
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 @@ -45,7 +101,7 @@ function FormReview() {
<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
Loading

0 comments on commit 5a37f2f

Please sign in to comment.