diff --git a/src/assets/feedback250x.png b/src/assets/feedback250x.png new file mode 100644 index 0000000..3e0f8fe Binary files /dev/null and b/src/assets/feedback250x.png differ diff --git a/src/assets/index.ts b/src/assets/index.ts index c6366b0..a05722c 100644 --- a/src/assets/index.ts +++ b/src/assets/index.ts @@ -1,6 +1,7 @@ import ahareBot from './ahare_bot250.png'; import faq from './faq250.png'; import shorts from './shorts250.png'; +import feedback from './feedback250x.png'; import balerina from './balerina250x.png'; import emotions from './emotions_promo250x.png'; import realism from './realism_promo250x.png'; @@ -38,6 +39,7 @@ export const images = { ahareBot, faq, shorts, + feedback, balerina, emotions, realism, diff --git a/src/components/shared/FeedbackHandler.tsx b/src/components/shared/FeedbackHandler.tsx new file mode 100644 index 0000000..4549e84 --- /dev/null +++ b/src/components/shared/FeedbackHandler.tsx @@ -0,0 +1,58 @@ +import React, { useState, forwardRef, useImperativeHandle } from 'react'; +import FeedbackModal from './FeedbackModal'; +import { sendFeedback } from '../../services/feedbackService'; +import { FeedbackData } from './FeedbackModal'; + +// Интерфейс для ref +export interface FeedbackHandlerRef { + openFeedbackModal: () => void; +} + +interface FeedbackHandlerProps { + onFeedbackSent?: () => void; // Callback после успешной отправки +} + +const FeedbackHandler = forwardRef((props, ref) => { + const { onFeedbackSent } = props; + + // Состояние для модального окна обратной связи + const [isFeedbackModalVisible, setIsFeedbackModalVisible] = useState(false); + + // Экспортируем метод openFeedbackModal через ref + useImperativeHandle(ref, () => ({ + openFeedbackModal: () => { + setIsFeedbackModalVisible(true); + } + })); + + // Обработчик отправки формы обратной связи + const handleFeedbackSubmit = async (data: FeedbackData): Promise => { + try { + const result = await sendFeedback(data); + console.log('Результат отправки обратной связи:', result); + + // Вызываем callback после успешной отправки + if (result && onFeedbackSent) { + onFeedbackSent(); + } + + return result; + } catch (error) { + console.error('Ошибка при отправке обратной связи:', error); + return false; + } + }; + + return ( + <> + {/* Модальное окно обратной связи */} + setIsFeedbackModalVisible(false)} + onSubmit={handleFeedbackSubmit} + /> + + ); +}); + +export default FeedbackHandler; diff --git a/src/components/shared/FeedbackModal.module.css b/src/components/shared/FeedbackModal.module.css new file mode 100644 index 0000000..ba454ed --- /dev/null +++ b/src/components/shared/FeedbackModal.module.css @@ -0,0 +1,114 @@ +.textarea { + width: calc(100% - var(--spacing-small) * 2); + min-height: 100px; + margin-bottom: var(--spacing-medium); + padding: var(--spacing-small); + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + resize: none; + font-family: inherit; + font-size: 14px; + box-sizing: border-box; +} + +.textarea:focus { + outline: none; + border-color: var(--color-primary); +} + +.imageUploadContainer { + margin-bottom: var(--spacing-medium); + width: 100%; + box-sizing: border-box; +} + +.addImageButton { + padding: var(--spacing-small) var(--spacing-medium); + background-color: var(--color-border); + border: none; + border-radius: var(--border-radius); + font-size: 14px; + cursor: pointer; + transition: background-color 0.2s; + width: auto; + max-width: 100%; + box-sizing: border-box; +} + +.addImageButton:hover { + background-color: #d0d0d0; +} + +.fileInput { + display: none; +} + +.imagePreviewContainer { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-small); + margin-top: var(--spacing-small); + width: 100%; + box-sizing: border-box; +} + +.imagePreview { + position: relative; + width: 80px; + height: 80px; + border-radius: var(--border-radius); + overflow: hidden; +} + +.imagePreview img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.removeImageButton { + position: absolute; + top: 4px; + right: 4px; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: rgba(0, 0, 0, 0.6); + color: white; + border: none; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + cursor: pointer; +} + +.errorMessage { + color: #e53935; + font-size: 14px; + margin-bottom: var(--spacing-medium); + padding: 8px; + background-color: rgba(229, 57, 53, 0.1); + border-radius: var(--border-radius); + text-align: center; +} + +/* Специальные стили для iOS */ +@supports (-webkit-touch-callout: none) { + .textarea { + -webkit-appearance: none; + -webkit-tap-highlight-color: transparent; + } + + /* Исправление для предотвращения проблем с прокруткой на iOS */ + .imagePreviewContainer { + -webkit-overflow-scrolling: touch; + } + + /* Дополнительные исправления для iOS */ + .modal { + width: calc(100% - var(--spacing-large) * 2); + margin: 0 var(--spacing-large); + max-width: 90vw; + } +} diff --git a/src/components/shared/FeedbackModal.tsx b/src/components/shared/FeedbackModal.tsx new file mode 100644 index 0000000..bdd3ffd --- /dev/null +++ b/src/components/shared/FeedbackModal.tsx @@ -0,0 +1,190 @@ +import React, { useState, useRef } from 'react'; +import styles from './NotificationModal.module.css'; // Используем существующие стили +import feedbackStyles from './FeedbackModal.module.css'; // Дополнительные стили для формы + +export interface FeedbackData { + text: string; + images: File[]; +} + +interface FeedbackModalProps { + isVisible: boolean; + onClose: () => void; + onSubmit: (data: FeedbackData) => Promise | boolean; +} + +const FeedbackModal: React.FC = ({ isVisible, onClose, onSubmit }) => { + const [text, setText] = useState(''); + const [images, setImages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const textareaRef = useRef(null); + const fileInputRef = useRef(null); + + // Сброс формы при закрытии + const handleClose = () => { + setText(''); + setImages([]); + setError(null); + onClose(); + }; + + // Функция для сворачивания клавиатуры + const dismissKeyboard = () => { + if (textareaRef.current) { + textareaRef.current.blur(); + } + }; + + // Обработчик нажатия клавиш + const handleKeyDown = (e: React.KeyboardEvent) => { + // Если нажата клавиша Enter (или Done на iOS) + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); // Предотвращаем добавление новой строки + dismissKeyboard(); // Сворачиваем клавиатуру + } + }; + + // Обработчик добавления изображений + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + const newFiles = Array.from(e.target.files); + setImages(prevImages => [...prevImages, ...newFiles]); + + // Сбрасываем значение input, чтобы можно было выбрать тот же файл повторно + e.target.value = ''; + } + }; + + // Обработчик удаления изображения + const handleRemoveImage = (index: number) => { + setImages(prevImages => prevImages.filter((_, i) => i !== index)); + }; + + // Обработчик отправки формы + const handleSubmit = async () => { + // Проверяем, что есть текст или изображения + if (!text.trim() && images.length === 0) { + setError('Пожалуйста, введите текст или добавьте изображение'); + return; + } + + try { + setIsLoading(true); + setError(null); + + const result = await onSubmit({ + text: text.trim(), + images + }); + + if (result) { + handleClose(); + } else { + setError('Не удалось отправить обратную связь. Пожалуйста, попробуйте позже.'); + } + } catch (err) { + console.error('Ошибка при отправке обратной связи:', err); + setError('Произошла ошибка при отправке. Пожалуйста, попробуйте позже.'); + } finally { + setIsLoading(false); + } + }; + + if (!isVisible) return null; + + return ( +
+
+
+
+ {isLoading && } + Обратная связь +
+
Расскажите нам о проблеме или предложении
+
+ + {/* Поле для ввода текста */} +