Add feedback form functionality
This commit is contained in:
parent
fa5f42e285
commit
1a4d7477cb
BIN
src/assets/feedback250x.png
Normal file
BIN
src/assets/feedback250x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@ -1,6 +1,7 @@
|
|||||||
import ahareBot from './ahare_bot250.png';
|
import ahareBot from './ahare_bot250.png';
|
||||||
import faq from './faq250.png';
|
import faq from './faq250.png';
|
||||||
import shorts from './shorts250.png';
|
import shorts from './shorts250.png';
|
||||||
|
import feedback from './feedback250x.png';
|
||||||
import balerina from './balerina250x.png';
|
import balerina from './balerina250x.png';
|
||||||
import emotions from './emotions_promo250x.png';
|
import emotions from './emotions_promo250x.png';
|
||||||
import realism from './realism_promo250x.png';
|
import realism from './realism_promo250x.png';
|
||||||
@ -38,6 +39,7 @@ export const images = {
|
|||||||
ahareBot,
|
ahareBot,
|
||||||
faq,
|
faq,
|
||||||
shorts,
|
shorts,
|
||||||
|
feedback,
|
||||||
balerina,
|
balerina,
|
||||||
emotions,
|
emotions,
|
||||||
realism,
|
realism,
|
||||||
|
|||||||
58
src/components/shared/FeedbackHandler.tsx
Normal file
58
src/components/shared/FeedbackHandler.tsx
Normal file
@ -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<FeedbackHandlerRef, FeedbackHandlerProps>((props, ref) => {
|
||||||
|
const { onFeedbackSent } = props;
|
||||||
|
|
||||||
|
// Состояние для модального окна обратной связи
|
||||||
|
const [isFeedbackModalVisible, setIsFeedbackModalVisible] = useState(false);
|
||||||
|
|
||||||
|
// Экспортируем метод openFeedbackModal через ref
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
openFeedbackModal: () => {
|
||||||
|
setIsFeedbackModalVisible(true);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Обработчик отправки формы обратной связи
|
||||||
|
const handleFeedbackSubmit = async (data: FeedbackData): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const result = await sendFeedback(data);
|
||||||
|
console.log('Результат отправки обратной связи:', result);
|
||||||
|
|
||||||
|
// Вызываем callback после успешной отправки
|
||||||
|
if (result && onFeedbackSent) {
|
||||||
|
onFeedbackSent();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при отправке обратной связи:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Модальное окно обратной связи */}
|
||||||
|
<FeedbackModal
|
||||||
|
isVisible={isFeedbackModalVisible}
|
||||||
|
onClose={() => setIsFeedbackModalVisible(false)}
|
||||||
|
onSubmit={handleFeedbackSubmit}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default FeedbackHandler;
|
||||||
114
src/components/shared/FeedbackModal.module.css
Normal file
114
src/components/shared/FeedbackModal.module.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
190
src/components/shared/FeedbackModal.tsx
Normal file
190
src/components/shared/FeedbackModal.tsx
Normal file
@ -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> | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeedbackModal: React.FC<FeedbackModalProps> = ({ isVisible, onClose, onSubmit }) => {
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
const [images, setImages] = useState<File[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Сброс формы при закрытии
|
||||||
|
const handleClose = () => {
|
||||||
|
setText('');
|
||||||
|
setImages([]);
|
||||||
|
setError(null);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для сворачивания клавиатуры
|
||||||
|
const dismissKeyboard = () => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик нажатия клавиш
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
// Если нажата клавиша Enter (или Done на iOS)
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault(); // Предотвращаем добавление новой строки
|
||||||
|
dismissKeyboard(); // Сворачиваем клавиатуру
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик добавления изображений
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.modal}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{isLoading && <span className={styles.spinner}></span>}
|
||||||
|
Обратная связь
|
||||||
|
</div>
|
||||||
|
<div className={styles.message}>Расскажите нам о проблеме или предложении</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Поле для ввода текста */}
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
className={feedbackStyles.textarea}
|
||||||
|
placeholder="Опишите вашу проблему или предложение..."
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
enterKeyHint="done"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Блок для загрузки изображений */}
|
||||||
|
<div className={feedbackStyles.imageUploadContainer}>
|
||||||
|
<button
|
||||||
|
className={feedbackStyles.addImageButton}
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
type="button"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Добавить изображение
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className={feedbackStyles.fileInput}
|
||||||
|
multiple
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Превью загруженных изображений */}
|
||||||
|
{images.length > 0 && (
|
||||||
|
<div className={feedbackStyles.imagePreviewContainer}>
|
||||||
|
{images.map((image, index) => (
|
||||||
|
<div key={index} className={feedbackStyles.imagePreview}>
|
||||||
|
<img src={URL.createObjectURL(image)} alt={`Preview ${index}`} />
|
||||||
|
<button
|
||||||
|
className={feedbackStyles.removeImageButton}
|
||||||
|
onClick={() => handleRemoveImage(index)}
|
||||||
|
type="button"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Сообщение об ошибке */}
|
||||||
|
{error && (
|
||||||
|
<div className={feedbackStyles.errorMessage}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Кнопки действий */}
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<button
|
||||||
|
className={`${styles.button} ${styles.secondaryButton}`}
|
||||||
|
onClick={handleClose}
|
||||||
|
type="button"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.button} ${styles.primaryButton}`}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
type="button"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Отправка...' : 'Отправить'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeedbackModal;
|
||||||
@ -9,17 +9,17 @@ export const homeScreenConfig: AppConfig = {
|
|||||||
id: 'mainActions',
|
id: 'mainActions',
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
id: 'invite',
|
id: 'feedback',
|
||||||
type: 'square',
|
type: 'square',
|
||||||
background: {
|
background: {
|
||||||
type: 'gradient',
|
type: 'gradient',
|
||||||
colors: ['#FF69B4', '#FF1493']
|
colors: ['#FF69B4', '#FF1493']
|
||||||
},
|
},
|
||||||
title: 'Поделиться',
|
title: 'Обратная связь',
|
||||||
imageUrl: images.ahareBot,
|
imageUrl: images.feedback,
|
||||||
action: {
|
action: {
|
||||||
type: 'function',
|
type: 'function',
|
||||||
value: 'inviteFriends'
|
value: 'sendFeedback'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useCallback, useEffect } from 'react';
|
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import BlockRenderer from '../components/blocks/BlockRenderer';
|
import BlockRenderer from '../components/blocks/BlockRenderer';
|
||||||
// import UploadPhotoBlock from '../components/blocks/UploadPhotoBlock'; // Не используется
|
// import UploadPhotoBlock from '../components/blocks/UploadPhotoBlock'; // Не используется
|
||||||
@ -7,6 +7,7 @@ import { homeScreenConfig } from '../config/homeScreen';
|
|||||||
import { stylePresets } from '../config/stylePresets';
|
import { stylePresets } from '../config/stylePresets';
|
||||||
import apiService from '../services/api';
|
import apiService from '../services/api';
|
||||||
import NotificationModal from '../components/shared/NotificationModal';
|
import NotificationModal from '../components/shared/NotificationModal';
|
||||||
|
import FeedbackHandler, { FeedbackHandlerRef } from '../components/shared/FeedbackHandler';
|
||||||
|
|
||||||
// Интерфейс для хранения данных о последней генерации
|
// Интерфейс для хранения данных о последней генерации
|
||||||
interface LastGenerationData {
|
interface LastGenerationData {
|
||||||
@ -18,6 +19,7 @@ interface LastGenerationData {
|
|||||||
|
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const feedbackHandlerRef = useRef<FeedbackHandlerRef>(null);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | undefined>(() => {
|
const [previewUrl, setPreviewUrl] = useState<string | undefined>(() => {
|
||||||
@ -223,6 +225,12 @@ const Home: React.FC = () => {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actionValue === 'sendFeedback') {
|
||||||
|
// Открываем модальное окно обратной связи
|
||||||
|
feedbackHandlerRef.current?.openFeedbackModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else if (actionType === 'route') {
|
} else if (actionType === 'route') {
|
||||||
// Добавляем обработку для действий типа 'route'
|
// Добавляем обработку для действий типа 'route'
|
||||||
navigate(actionValue);
|
navigate(actionValue);
|
||||||
@ -293,6 +301,18 @@ const Home: React.FC = () => {
|
|||||||
onContinueClick={handleContinueClick}
|
onContinueClick={handleContinueClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Компонент обработки обратной связи */}
|
||||||
|
<FeedbackHandler
|
||||||
|
ref={feedbackHandlerRef}
|
||||||
|
onFeedbackSent={() => {
|
||||||
|
// Показываем уведомление об успешной отправке
|
||||||
|
setNotificationTitle('Спасибо за обратную связь');
|
||||||
|
setNotificationMessage('Ваше сообщение успешно отправлено');
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsNotificationVisible(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* Блоки из конфигурации */}
|
{/* Блоки из конфигурации */}
|
||||||
<div className={styles.blocks}>
|
<div className={styles.blocks}>
|
||||||
|
|||||||
75
src/services/feedbackService.ts
Normal file
75
src/services/feedbackService.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { FeedbackData } from '../components/shared/FeedbackModal';
|
||||||
|
|
||||||
|
// Функция для преобразования File в base64
|
||||||
|
const fileToBase64 = (file: File): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => {
|
||||||
|
// Получаем base64 строку и удаляем префикс (data:image/jpeg;base64,)
|
||||||
|
const base64String = reader.result as string;
|
||||||
|
const base64Content = base64String.split(',')[1];
|
||||||
|
resolve(base64Content);
|
||||||
|
};
|
||||||
|
reader.onerror = error => reject(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для получения информации о пользователе из Telegram
|
||||||
|
const getUserInfo = () => {
|
||||||
|
if (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initDataUnsafe) {
|
||||||
|
const { user } = window.Telegram.WebApp.initDataUnsafe;
|
||||||
|
if (user) {
|
||||||
|
return {
|
||||||
|
user_id: user.id.toString(),
|
||||||
|
username: user.username || `${user.first_name} ${user.last_name || ''}`.trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заглушка для тестирования
|
||||||
|
return {
|
||||||
|
user_id: "test_user_id",
|
||||||
|
username: "test_user"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для отправки данных обратной связи
|
||||||
|
export const sendFeedback = async (data: FeedbackData): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
// Получаем информацию о пользователе
|
||||||
|
const userInfo = getUserInfo();
|
||||||
|
|
||||||
|
// Преобразуем все изображения в base64
|
||||||
|
const base64Images = await Promise.all(data.images.map(fileToBase64));
|
||||||
|
|
||||||
|
// Формируем данные для отправки
|
||||||
|
const requestData = {
|
||||||
|
text: data.text,
|
||||||
|
user_id: userInfo.user_id,
|
||||||
|
username: userInfo.username,
|
||||||
|
images: base64Images
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Отправка данных обратной связи:', requestData);
|
||||||
|
|
||||||
|
// Отправляем запрос
|
||||||
|
const response = await fetch('https://feedbacksticker.gymnasticstuff.uk/feedback', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Обратная связь успешно отправлена');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при отправке обратной связи:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
78
src/types/telegram-webapp.d.ts
vendored
Normal file
78
src/types/telegram-webapp.d.ts
vendored
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
interface TelegramWebAppUser {
|
||||||
|
id: number;
|
||||||
|
is_bot?: boolean;
|
||||||
|
first_name: string;
|
||||||
|
last_name?: string;
|
||||||
|
username?: string;
|
||||||
|
language_code?: string;
|
||||||
|
photo_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TelegramWebAppInitData {
|
||||||
|
query_id?: string;
|
||||||
|
user?: TelegramWebAppUser;
|
||||||
|
auth_date?: string;
|
||||||
|
hash?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TelegramWebApp {
|
||||||
|
initData: string;
|
||||||
|
initDataUnsafe: TelegramWebAppInitData;
|
||||||
|
version: string;
|
||||||
|
colorScheme: 'light' | 'dark';
|
||||||
|
themeParams: {
|
||||||
|
bg_color: string;
|
||||||
|
text_color: string;
|
||||||
|
hint_color: string;
|
||||||
|
link_color: string;
|
||||||
|
button_color: string;
|
||||||
|
button_text_color: string;
|
||||||
|
};
|
||||||
|
isExpanded: boolean;
|
||||||
|
viewportHeight: number;
|
||||||
|
viewportStableHeight: number;
|
||||||
|
headerColor: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
ready(): void;
|
||||||
|
expand(): void;
|
||||||
|
close(): void;
|
||||||
|
openLink(url: string): void;
|
||||||
|
openTelegramLink(url: string): void;
|
||||||
|
showAlert(message: string, callback?: () => void): void;
|
||||||
|
showConfirm(message: string, callback?: (confirmed: boolean) => void): void;
|
||||||
|
MainButton: {
|
||||||
|
text: string;
|
||||||
|
color: string;
|
||||||
|
textColor: string;
|
||||||
|
isVisible: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
isProgressVisible: boolean;
|
||||||
|
setText(text: string): void;
|
||||||
|
onClick(callback: () => void): void;
|
||||||
|
offClick(callback: () => void): void;
|
||||||
|
show(): void;
|
||||||
|
hide(): void;
|
||||||
|
enable(): void;
|
||||||
|
disable(): void;
|
||||||
|
showProgress(leaveActive: boolean): void;
|
||||||
|
hideProgress(): void;
|
||||||
|
};
|
||||||
|
BackButton: {
|
||||||
|
isVisible: boolean;
|
||||||
|
onClick(callback: () => void): void;
|
||||||
|
offClick(callback: () => void): void;
|
||||||
|
show(): void;
|
||||||
|
hide(): void;
|
||||||
|
};
|
||||||
|
HapticFeedback: {
|
||||||
|
impactOccurred(style: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft'): void;
|
||||||
|
notificationOccurred(type: 'error' | 'success' | 'warning'): void;
|
||||||
|
selectionChanged(): void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
Telegram?: {
|
||||||
|
WebApp: TelegramWebApp;
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user