Skip to content

Commit

Permalink
Merge pull request #14 from antoshkaxxr/module8-task1
Browse files Browse the repository at this point in the history
  • Loading branch information
keksobot authored Dec 3, 2024
2 parents f01eae9 + 710b80a commit c0d07d6
Show file tree
Hide file tree
Showing 43 changed files with 368 additions and 372 deletions.
7 changes: 4 additions & 3 deletions src/components/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import {AppRoute} from '../../const.ts';
import {LoginPage} from '../../pages/login-page/login-page.tsx';
import {PrivateRoute} from '../private-route/private-route.tsx';
import {OfferPage} from '../../pages/offer-page/offer-page.tsx';
import {NotFoundPage} from '../../pages/404-not-found-page/not-found-page.tsx';
import {NotFoundPage} from '../../pages/not-found-page/not-found-page.tsx';
import {FavoritesPage} from '../../pages/favorites-page/favorites-page.tsx';
import {useAppSelector} from '../../hooks';
import {Spinner} from '../spinner/spinner.tsx';
import {HistoryRouter} from '../history-route/history-route.tsx';
import {browserHistory} from '../../browser-history.ts';
import {PrivateLoginRoute} from '../private-login-route/private-login-route.tsx';
import {getDataLoadingStatus} from '../../store/offers-data/selectors.ts';

export function App(): JSX.Element {
const isOffersDataLoading = useAppSelector((state) => state.isDataLoading);
export function App() {
const isOffersDataLoading = useAppSelector(getDataLoadingStatus);

if (isOffersDataLoading) {
return (
Expand Down
5 changes: 4 additions & 1 deletion src/components/card/card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Offer} from '../../types/offer.ts';
import {Link} from 'react-router-dom';
import {AppRoute} from '../../const.ts';
import {memo} from 'react';

type CardProps = {
offer: Offer;
Expand All @@ -9,7 +10,7 @@ type CardProps = {
isNearby?: boolean;
}

export function Card({offer, onMouseEnter, onMouseLeave, isNearby = false}: CardProps): JSX.Element {
function CardComponent({offer, onMouseEnter, onMouseLeave, isNearby = false}: CardProps) {
const stylePrefix = isNearby ? 'near-places' : 'cities';
return (
<Link to={`${AppRoute.Offer}/${offer.id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
Expand Down Expand Up @@ -56,3 +57,5 @@ export function Card({offer, onMouseEnter, onMouseLeave, isNearby = false}: Card
</Link>
);
}

export const Card = memo(CardComponent);
6 changes: 5 additions & 1 deletion src/components/cities-list/cities-list.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {memo} from 'react';

type CitiesListProps = {
cities: string[];
activeCity: string;
onCityChange: (city: string) => void;
};

export function CitiesList({ cities, activeCity, onCityChange }: CitiesListProps): JSX.Element {
function CitiesListComponent({ cities, activeCity, onCityChange }: CitiesListProps) {
return (
<section className="locations container">
<ul className="locations__list tabs__list">
Expand All @@ -26,3 +28,5 @@ export function CitiesList({ cities, activeCity, onCityChange }: CitiesListProps
</section>
);
}

export const CitiesList = memo(CitiesListComponent);
57 changes: 34 additions & 23 deletions 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, useState} from 'react';
import {useAppDispatch} from '../../hooks';
import {postCommentAction} from '../../store/api-actions.ts';
import {showCustomToast} from '../custom-toast/custom-toast.tsx';
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';

const getRatingTitle = (star: number) => {
switch (star) {
Expand All @@ -22,41 +22,46 @@ type CommentFormProps = {
offerId: string;
}

export function CommentForm({offerId}: CommentFormProps) {
function CommentFormComponent({ offerId }: CommentFormProps) {
const [formData, setFormData] = useState({
rating: '',
review: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);

const dispatch = useAppDispatch();

const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const {name, value} = e.target;
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData((prevState) => ({
...prevState,
[name]: value
}));
};
}, []);

const handleSubmit = (e: FormEvent) => {
const handleSubmitAsync = useCallback(async (e: FormEvent) => {
e.preventDefault();
const {rating, review} = formData;
const { rating, review } = formData;

if (!rating) {
showCustomToast('Please select a rating.');
return;
}
setIsSubmitting(true);

if (review.length < 50 || review.length > 300) {
showCustomToast('The review must contain from 50 to 300 characters.');
return;
try {
await dispatch(postCommentAction({ offerId, comment: review, rating: Number(rating) }));
setFormData({ rating: '', review: '' });
} catch (error) {
showCustomToast('Failed to submit the review. Please try again.');
} finally {
setIsSubmitting(false);
}
}, [dispatch, formData, offerId]);

const handleSubmit = useCallback((e: FormEvent) => {
handleSubmitAsync(e).catch(() => {
showCustomToast('Failed to submit the review. Please try again.');
});
}, [handleSubmitAsync]);

dispatch(postCommentAction({offerId, comment: review, rating: Number(rating)}))
.then(() => {
setFormData({rating: '', review: ''});
});
};
const isSubmitDisabled = !formData.rating || formData.review.length < 50 || formData.review.length > 300 || isSubmitting;

return (
<form className="reviews__form form" onSubmit={handleSubmit}>
Expand All @@ -66,6 +71,7 @@ export function CommentForm({offerId}: CommentFormProps) {
<React.Fragment key={star}>
<input className="form__rating-input visually-hidden" name="rating" value={`${star}`} id={`${star}-stars`}
type="radio" onChange={handleChange} checked={formData.rating === `${star}`}
disabled={isSubmitting}
/>
<label htmlFor={`${star}-stars`} className="reviews__rating-label form__rating-label"
title={getRatingTitle(star)}
Expand All @@ -81,15 +87,20 @@ export function CommentForm({offerId}: CommentFormProps) {
placeholder="Tell how was your stay, what you like and what can be improved"
value={formData.review}
onChange={handleChange}
disabled={isSubmitting}
>
</textarea>
<div className="reviews__button-wrapper">
<p className="reviews__help">
To submit review please make sure to set <span className="reviews__star">rating</span>&nbsp;
and describe your stay <b className="reviews__text-amount">from 50 to 300 characters</b>.
</p>
<button className="reviews__submit form__submit button" type="submit">Submit</button>
<button className="reviews__submit form__submit button" type="submit" disabled={isSubmitDisabled}>
Submit
</button>
</div>
</form>
);
}

export const CommentForm = memo(CommentFormComponent);
6 changes: 5 additions & 1 deletion src/components/favorite-card/favorite-card.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {Offer} from '../../types/offer.ts';
import {Link} from 'react-router-dom';
import {AppRoute} from '../../const.ts';
import {memo} from 'react';


type FavoriteCardProps = {
offer: Offer;
}

export function FavoriteCard({offer}: FavoriteCardProps): JSX.Element {
function FavoriteCardComponent({offer}: FavoriteCardProps) {
return (
<article className={'favorites__card place-card'}>
{offer.isPremium &&
Expand Down Expand Up @@ -48,3 +50,5 @@ export function FavoriteCard({offer}: FavoriteCardProps): JSX.Element {
</article>
);
}

export const FavoriteCard = memo(FavoriteCardComponent);
7 changes: 4 additions & 3 deletions src/components/favorites-list/favorites-list.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {useMemo} from 'react';
import {Offer} from '../../types/offer.ts';
import {FavoriteCard} from '../favorite-card/favorite-card.tsx';

type FavoritesListProps = {
offers: Offer[];
}

export function FavoritesList({offers} : FavoritesListProps) {
const groupedOffers = offers.reduce((acc, offer) => {
export function FavoritesList({offers}: FavoritesListProps) {
const groupedOffers = useMemo(() => offers.reduce((acc, offer) => {
const cityName = offer.city.name;

if (!acc[cityName]) {
Expand All @@ -16,7 +17,7 @@ export function FavoritesList({offers} : FavoritesListProps) {
acc[cityName].push(offer);

return acc;
}, {} as Record<string, Offer[]>);
}, {} as Record<string, Offer[]>), [offers]);

return (
<ul className="favorites__list">
Expand Down
18 changes: 11 additions & 7 deletions src/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import {useAppDispatch, useAppSelector} from '../../hooks';
import {Link, useLocation} from 'react-router-dom';
import {AppRoute} from '../../const.ts';
import {logoutAction} from '../../store/api-actions.ts';
import {memo} from 'react';
import {getFavoriteOffers, getUserInfo} from '../../store/user-data/selectors.ts';

export function Header() {
function HeaderComponent() {
const dispatch = useAppDispatch();
const userData = useAppSelector((state) => state.userData);
const favoriteOffers = useAppSelector((state) => state.favoriteOffers);
const userInfo = useAppSelector(getUserInfo);
const favoriteOffers = useAppSelector(getFavoriteOffers);
const location = useLocation();

const isLoginPage = location.pathname === AppRoute.Login as string;
Expand All @@ -24,12 +26,12 @@ export function Header() {
<nav className="header__nav">
<ul className="header__nav-list">
<li className="header__nav-item user">
{userData ?
{userInfo ?
<Link to={AppRoute.Favorites} className="header__nav-link header__nav-link--profile">
<div className="header__avatar-wrapper user__avatar-wrapper">
<img className="user__avatar" src={userData.avatarUrl} alt="avatar" />
<img className="user__avatar" src={userInfo.avatarUrl} alt="avatar" />
</div>
<span className="header__user-name user__name">{userData.email}</span>
<span className="header__user-name user__name">{userInfo.email}</span>
<span className="header__favorite-count">{favoriteOffers.length}</span>
</Link>
:
Expand All @@ -39,7 +41,7 @@ export function Header() {
<span className="header__login">Sign in</span>
</Link>}
</li>
{userData &&
{userInfo &&
<li className="header__nav-item">
<Link
className="header__nav-link"
Expand All @@ -60,3 +62,5 @@ export function Header() {
</header>
);
}

export const Header = memo(HeaderComponent);
16 changes: 9 additions & 7 deletions src/components/login-form/login-form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {FormEvent, useRef} from 'react';
import {useAppDispatch} from '../../hooks';
import {loginAction} from '../../store/api-actions.ts';
import {showCustomToast} from '../custom-toast/custom-toast.tsx';
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';

const validatePassword = (password: string): boolean => {
const hasLetter = /[a-zA-Z]/.test(password);
Expand All @@ -11,12 +11,12 @@ const validatePassword = (password: string): boolean => {
return hasLetter && hasNumber && hasNoSpaces;
};

export function LoginForm() {
function LoginFormComponent() {
const loginFormRef = useRef<HTMLFormElement>(null);

const dispatch = useAppDispatch();

const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
const handleSubmit = useCallback((evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();

if (!loginFormRef.current) {
Expand All @@ -38,7 +38,7 @@ export function LoginForm() {
password
}));
}
};
}, [dispatch]);

return (
<section className="login">
Expand All @@ -62,3 +62,5 @@ export function LoginForm() {
</section>
);
}

export const LoginForm = memo(LoginFormComponent);
36 changes: 19 additions & 17 deletions src/components/map/map.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import {useEffect, useRef} from 'react';
import {useMap} from '../../hooks/useMap/useMap';
import { useEffect, useRef, memo } from 'react';
import { useMap } from '../../hooks/useMap/useMap';
import leaflet from 'leaflet';
import 'leaflet/dist/leaflet.css';
import {Point} from '../../types/offer.ts';
import { Point } from '../../types/offer.ts';

type MapProps = {
points: Point[];
activePointId: string | null;
height: number;
}

export function Map({points, activePointId, height} : MapProps) {
const mapRef = useRef(null);
const map = useMap(mapRef, points[0].city);
const defaultCustomIcon = leaflet.icon({
iconUrl: '/img/pin.svg',
iconSize: [40, 40],
iconAnchor: [20, 40],
});

const defaultCustomIcon = leaflet.icon({
iconUrl: '/img/pin.svg',
iconSize: [40, 40],
iconAnchor: [20, 40],
});
const currentCustomIcon = leaflet.icon({
iconUrl: '/img/pin-active.svg',
iconSize: [40, 40],
iconAnchor: [20, 40],
});

const currentCustomIcon = leaflet.icon({
iconUrl: '/img/pin-active.svg',
iconSize: [40, 40],
iconAnchor: [20, 40],
});
function MapComponent ({ points, activePointId, height }: MapProps) {
const mapRef = useRef(null);
const map = useMap(mapRef, points[0].city);

useEffect(() => {
if (map) {
Expand All @@ -51,9 +51,11 @@ export function Map({points, activePointId, height} : MapProps) {

return (
<div
style={{height: `${height}px`}}
style={{ height: `${height}px` }}
ref={mapRef}
>
</div>
);
}

export const Map = memo(MapComponent);
9 changes: 6 additions & 3 deletions src/components/offers-list/offers-list.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {Offer} from '../../types/offer';
import {Card} from '../card/card';
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;
}

export function OffersList({offers, setActiveOfferId, isNearby = false}: OffersListProps) {
function OffersListComponent({ offers, setActiveOfferId, isNearby = false }: OffersListProps) {
const handleMouseEnter = (id: string) => {
setActiveOfferId(id);
};
Expand All @@ -31,3 +32,5 @@ export function OffersList({offers, setActiveOfferId, isNearby = false}: OffersL
</div>
);
}

export const OffersList = memo(OffersListComponent);
Loading

0 comments on commit c0d07d6

Please sign in to comment.