Сделал три модалки с похожей логикой, но не могу понять почему у двух ModalPrice и ModalProduct анимация при закрытии и открытии работает, а у ModalReview не работает. Не один день уже потратил - ChatGTP не видит видимых проблем, в консоли и терминале ошибок нет. Буду очень признателен за помощь.
//ModalPrice.js:
import React, { useEffect, useCallback, useState } from 'react';
import icons from './icons.svg';
import './styles.css';
const ModalPrice = ({ imgSrc, isOpen, onRequestClose }) => {
const [isClosing, setIsClosing] = useState(false);
const closeModal = useCallback(() => {
setIsClosing(true);
setTimeout(() => onRequestClose(), 500);
}, [onRequestClose]);
useEffect(() => {
const handleEscape = e => isOpen && e.key === 'Escape' && closeModal();
document.body.classList.toggle('modal-open', isOpen);
document.body.style.overflow = isOpen ? 'hidden' : 'auto';
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'auto';
setIsClosing(false);
};
}, [isOpen, closeModal]);
const overlayClassName = isOpen ? 'modal_overlay active' : 'modal_overlay';
const modalClassName = isClosing
? 'modal_price closing'
: isOpen
? 'modal_price active'
: 'modal_price';
return (
<div
className={`${overlayClassName}`}
id="modalOverlay"
onClick={closeModal}
>
<div className={`${modalClassName}`} onClick={e => e.stopPropagation()}>
<button onClick={closeModal} className="close_modal">
<svg className="icon_modal">
<use xlinkHref={`${icons}#close`} />
</svg>
</button>
<div className="image_container">
<img src={imgSrc} alt="ПРАЙС - ЛИСТ" className="modal_image" />
</div>
</div>
</div>
);
};
export default ModalPrice;
//ModalProduct.js:
import React, { useEffect, useState, useCallback } from 'react';
import Slider from 'react-slick';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
import icons from './icons.svg';
import './styles.css';
const PrevArrow = ({ onClick }) => (
<div className="slide_arrow prev" onClick={onClick}>
<svg className="icon_modal">
<use xlinkHref={`${icons}#left`} />
</svg>
</div>
);
const NextArrow = ({ onClick }) => (
<div className="slide_arrow next" onClick={onClick}>
<svg className="icon_modal">
<use xlinkHref={`${icons}#right`} />
</svg>
</div>
);
const ModalProduct = ({ images, closeModal }) => {
const [modalVisible, setModalVisible] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const closeAnimation = useCallback(() => {
setIsClosing(true);
setTimeout(() => {
setModalVisible(false);
closeModal();
}, 500);
}, [closeModal]);
useEffect(() => {
const closeOnEsc = event => {
if (event.key === 'Escape') {
closeAnimation();
}
};
document.body.addEventListener('keydown', closeOnEsc);
document.body.style.overflow = 'hidden';
return () => {
document.body.removeEventListener('keydown', closeOnEsc);
document.body.style.overflow = 'auto';
};
}, [closeAnimation]);
const settings = {
dots: true,
speed: 1500,
slidesToShow: 1,
slidesToScroll: 1,
autoplay: true,
autoplaySpeed: 3000,
infinite: true,
pauseOnHover: false,
prevArrow: <PrevArrow />,
nextArrow: <NextArrow />,
};
useEffect(() => {
setModalVisible(true);
}, []);
const overlayClassName = modalVisible
? 'modal_overlay active'
: 'modal_overlay';
const modalClassName = isClosing
? 'modal_product closing'
: modalVisible
? 'modal_product active'
: 'modal_product';
return (
<div className={`${overlayClassName}`} onClick={closeAnimation}>
<div className={`${modalClassName}`} onClick={e => e.stopPropagation()}>
<button type="button" className="close_modal" onClick={closeAnimation}>
<svg className="icon_modal">
<use xlinkHref={`${icons}#close`} />
</svg>
</button>
<Slider {...settings}>
{images.map((imageData, index) => (
<div key={index} className="slide">
<img
className="slide_img"
src={imageData.imageSrc}
alt={`Slide ${index + 1}`}
/>
<p
style={{
textAlign: 'center',
marginTop: '10px',
width: '100%',
}}
>
{imageData.text.includes('(') ? (
<>
{imageData.text.substring(0, imageData.text.indexOf('('))}
<span style={{ fontWeight: 'bold', color: 'red' }}>
{imageData.text.substring(imageData.text.indexOf('('))}
</span>
</>
) : (
imageData.text
)}
</p>
</div>
))}
</Slider>
</div>
</div>
);
};
export default ModalProduct;
// ModalReview.js:
import React, { useState, useEffect, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { setReviews } from '../../redux/actions';
import { addReviewApi, fetchReviewsApi } from '../../api/api';
import Notiflix from 'notiflix';
import icons from './icons.svg';
import './styles.css';
const ModalReview = ({ closeModal, onSuccess }) => {
const [newReview, setNewReview] = useState({ name: '', comment: '' });
const [formErrors, setFormErrors] = useState({});
const dispatch = useDispatch();
const [isClosing, setIsClosing] = useState(false);
useEffect(() => {
document.body.style.overflow = isClosing ? 'auto' : 'hidden';
}, [isClosing]);
const closeModalWithAnimation = useCallback(() => {
setIsClosing(true);
setTimeout(() => {
setIsClosing(false);
closeModal();
}, 500);
}, [closeModal]);
const handleEscape = useCallback(
e => {
if (e.key === 'Escape') {
closeModalWithAnimation();
}
},
[closeModalWithAnimation]
);
useEffect(() => {
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [handleEscape]);
const validateForm = () => {
const errors = {};
if (!newReview.name.trim()) {
errors.name = 'Не введено имя.';
}
if (!newReview.comment.trim()) {
errors.comment = 'Не введен комментарий.';
}
return errors;
};
const handleInputChange = e => {
const { name, value } = e.target;
setNewReview({ ...newReview, [name]: value });
};
const handleSubmit = async e => {
e.preventDefault();
const errors = validateForm();
setFormErrors(errors);
if (Object.keys(errors).length === 0) {
try {
await addReviewApi(newReview);
const reviews = await fetchReviewsApi();
dispatch(setReviews(reviews));
onSuccess();
closeModalWithAnimation();
} catch (error) {
console.error('Ошибка при добавлении отзыва:', error);
}
} else {
Notiflix.Notify.failure('Будь ласка, заповніть усі поля форми.', {
position: 'center-center',
timeout: 2000,
});
}
};
return (
<div
className={`modale_overlay ${isClosing ? 'closing' : ''} ${
isClosing ? '' : 'active'
}`}
onClick={closeModalWithAnimation}
>
<div
className={`modale_content ${isClosing ? 'closing' : ''} ${
isClosing ? '' : 'active'
}`}
onClick={e => e.stopPropagation()}
>
<button className="close_modal" onClick={closeModalWithAnimation}>
<svg className="icon_modal">
<use xlinkHref={`${icons}#close`} />
</svg>
</button>
<form className="reviews_form" onSubmit={handleSubmit}>
<label className="reviews_label">
Ім'я:
<input
className="reviews_input"
type="text"
name="name"
value={newReview.name}
autoComplete="name"
onChange={handleInputChange}
/>
{formErrors.name && (
<div style={{ display: 'none' }}>{formErrors.name}</div>
)}
</label>
<br />
<label>
Коментар:
<textarea
className="reviews_textarea"
name="comment"
value={newReview.comment}
autoComplete="comment"
onChange={handleInputChange}
/>
{formErrors.comment && (
<div style={{ display: 'none' }}>{formErrors.comment}</div>
)}
</label>
<br />
<button className="reviews_button modale" type="submit">
ВІДПРАВИТИ ВІДГУК
</button>
</form>
</div>
</div>
);
};
export default ModalReview;
и общий файл со стилями для этих модалок:
/* !-------------------Modal All-------------------------- */
.modal_overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--color-overlay-body);
z-index: 2000;
opacity: 0;
visibility: hidden;
}
.modal_overlay.active {
opacity: 1;
visibility: visible;
}
.image_container {
overflow-y: auto;
max-height: calc(100vh - 150px);
margin-top: 10px;
}
.modal_image {
width: 100%;
height: auto;
object-fit: contain;
}
.close_modal {
position: absolute;
top: 8px;
right: 8px;
display: flex;
justify-content: center;
align-items: center;
text-decoration: none;
border-radius: 50%;
border: 1px solid var(--color-backdrop);
width: 30px;
height: 30px;
cursor: pointer;
padding: 0;
margin: 0;
color: var(--color-general);
background-color: var(--background-general);
transition: color var(--transition-timing) var(--transition-duration);
}
.close_modal:hover,
.close_modal:focus {
color: var(--color-nav-link);
}
.icon_modal {
width: 20px;
height: 20px;
fill: currentColor;
align-items: center;
}
/* !-----------------Modal Price-------------------------- */
.modal_price {
position: fixed;
top: 50%;
left: 50%;
width: 95%;
height: 100%;
max-width: 820px;
max-height: 680px;
background-color: var(--background-slogan);
border-radius: 8px;
padding: 40px;
margin: 0;
z-index: 3000;
transform: translate(-50%, -50%) scale(0);
opacity: 0;
transition: opacity var(--transition-duration-product),
transform var(--transition-duration-product);
}
.modal_price.active {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
.modal_price.closing {
opacity: 0;
transform: translate(-50%, -50%) scale(0);
transition: opacity var(--transition-duration-product),
transform var(--transition-duration-product);
}
/* !---------------Modal Product-------------------------- */
.modal_product {
position: fixed;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
max-width: 550px;
max-height: 630px;
background-color: var(--background-slogan);
border-radius: 8px;
padding: 40px;
margin: 0;
z-index: 1000;
transform: translate(-50%, -50%) scale(0);
opacity: 0;
transition: transform var(--transition-timing) var(--transition-duration-product),
opacity var(--transition-timing) var(--transition-duration-product);
}
.modal_product.active {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
.modal_product.closing {
opacity: 0;
transform: translate(-50%, -50%) scale(0);
transition: opacity var(--transition-duration-product),
transform var(--transition-duration-product);
}
@media screen and (max-width: 1200px) {
.modal_product {
max-width: 450px;
max-height: 580px;
}
}
/* !---------------Slider Product-------------------------- */
.slide {
margin-top: 20px;
}
.slide_img {
width: 100%;
height: 100%;
margin: 0 auto;
max-width: 400px;
max-height: 500px;
border-radius: 8px;
object-fit: contain;
object-position: center;
}
.slide_arrow {
position: absolute;
top: 260px;
width: 40px;
height: 40px;
background-color: var(--background-general);
color: var(--color-general);
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--color-backdrop);
border-radius: 50%;
cursor: pointer;
transition: color var(--transition-timing) var(--transition-duration);
z-index: 1;
}
.slide_arrow.prev {
left: 5px;
}
.slide_arrow.next {
right: 5px;
}
.slide_arrow:hover,
.slide_arrow:focus {
color: var(--color-nav-link);
}
/* !---------------Modal Review-------------------------- */
.modale_overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--color-overlay-body);
z-index: 2000;
opacity: 0;
visibility: hidden;
}
.modale_overlay.active {
opacity: 1;
visibility: visible;
}
.modale_content {
position: fixed;
top: 50%;
left: 50%;
background-color: var(--background-slogan);
padding: 40px;
padding-top: 20px;
border-radius: 8px;
z-index: 1051;
transform: translate(-50%, -50%) scale(0);
opacity: 0;
visibility: hidden;
transition: transform 500ms, opacity 500ms;
}
.modale_content.active {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
visibility: visible;
transition-delay: 0ms;
}
.modale_content.closing {
opacity: 0;
visibility: hidden;
transform: translate(-50%, -50%) scale(0);
transition: transform 500ms, opacity 500ms;
}