@@ -13,7 +50,10 @@ const CreateSticker: React.FC = () => {
-
+
📷
@@ -22,8 +62,24 @@ const CreateSticker: React.FC = () => {
или перетащите файл сюда
+
+ Стоимость: {TOKENS_PER_GENERATION} токенов
+
+
+
setShowTokensModal(false)}
+ onShowAllPacks={() => navigate('/profile')}
+ missingTokens={missingTokens}
+ onBuyPack={(packId: string) => {
+ setShowTokensModal(false);
+ paymentService.showBuyTokensPopup(tokenPacks.find(p => p.id === packId)!, () => {
+ window.location.reload();
+ });
+ }}
+ />
);
};
diff --git a/src/screens/Home.tsx b/src/screens/Home.tsx
index d7fef58..dc05111 100644
--- a/src/screens/Home.tsx
+++ b/src/screens/Home.tsx
@@ -1,13 +1,16 @@
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import BlockRenderer from '../components/blocks/BlockRenderer';
-// import UploadPhotoBlock from '../components/blocks/UploadPhotoBlock'; // Не используется
import styles from './Home.module.css';
import { homeScreenConfig } from '../config/homeScreen';
import { stylePresets } from '../config/stylePresets';
import apiService from '../services/api';
import NotificationModal from '../components/shared/NotificationModal';
import FeedbackHandler, { FeedbackHandlerRef } from '../components/shared/FeedbackHandler';
+import TokenPacksModal from '../components/tokens/TokenPacksModal';
+import { paymentService } from '../services/paymentService';
+import { tokenPacks } from '../constants/tokenPacks';
+import { getCurrentUserId } from '../constants/user';
// Интерфейс для хранения данных о последней генерации
interface LastGenerationData {
@@ -45,11 +48,11 @@ const Home: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [promptText, setPromptText] = useState('');
const [showGalleryButton, setShowGalleryButton] = useState(true);
- const [showButtons, setShowButtons] = useState(true); // Новое состояние для управления видимостью всех кнопок
- const [continueButtonText, setContinueButtonText] = useState('Продолжить'); // Новое состояние для текста кнопки "Продолжить"
-
- // Состояние для хранения данных о последней успешной генерации
+ const [showButtons, setShowButtons] = useState(true);
+ const [continueButtonText, setContinueButtonText] = useState('Продолжить');
const [lastGenerationData, setLastGenerationData] = useState
({});
+ const [showTokensModal, setShowTokensModal] = useState(false);
+ const [missingTokens, setMissingTokens] = useState(0);
// Обработчики для модального окна
const handleGalleryClick = useCallback(() => {
@@ -152,14 +155,24 @@ const Home: React.FC = () => {
}
try {
+ // Проверяем баланс перед генерацией
+ const userTokens = await apiService.getBalance(getCurrentUserId());
+ const TOKENS_PER_GENERATION = 10;
+
+ if (userTokens < TOKENS_PER_GENERATION) {
+ setMissingTokens(TOKENS_PER_GENERATION - userTokens);
+ setShowTokensModal(true);
+ return;
+ }
+
// Показываем уведомление о начале генерации
setNotificationTitle('Генерация стикера');
setNotificationMessage('Отправка запроса...');
setIsLoading(true);
setPromptText('');
- setShowGalleryButton(true); // Показываем кнопку "В галерею" для уведомлений о генерации
- setShowButtons(false); // Скрываем все кнопки во время отправки запроса
- setContinueButtonText('Продолжить'); // Сбрасываем текст кнопки на значение по умолчанию
+ setShowGalleryButton(true);
+ setShowButtons(false);
+ setContinueButtonText('Продолжить');
setIsNotificationVisible(true);
// Если выбран "Свой промпт" и введен текст, используем его
@@ -356,6 +369,19 @@ const Home: React.FC = () => {
/>
+ {/* Модальное окно с пакетами токенов */}
+
setShowTokensModal(false)}
+ onShowAllPacks={() => navigate('/profile')}
+ missingTokens={missingTokens}
+ onBuyPack={(packId: string) => {
+ setShowTokensModal(false);
+ paymentService.showBuyTokensPopup(tokenPacks.find(p => p.id === packId)!, () => {
+ window.location.reload();
+ });
+ }}
+ />
{/* Блоки из конфигурации */}
{homeScreenConfig.homeScreen.blocks
diff --git a/src/screens/Profile.module.css b/src/screens/Profile.module.css
index 0f8a2a2..e7fe8a6 100644
--- a/src/screens/Profile.module.css
+++ b/src/screens/Profile.module.css
@@ -1,63 +1,75 @@
.container {
display: flex;
flex-direction: column;
- gap: var(--spacing-large);
- padding: calc(3rem + var(--spacing-small)) var(--spacing-medium) var(--spacing-large);
+ gap: 12px;
+ padding: calc(3rem + var(--spacing-small)) var(--spacing-medium) 6rem;
width: 100%;
box-sizing: border-box;
}
.header {
- padding: var(--spacing-small) 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
text-align: center;
- margin-bottom: 0;
+ padding: 8px 0;
}
.title {
font-size: 1.5rem;
font-weight: bold;
color: var(--color-text);
- display: inline-block;
+ margin: 0;
}
.subtitle {
- margin-top: 0.25rem;
+ margin: 0;
color: var(--color-text);
opacity: 0.6;
- text-align: center;
+ font-size: 0.875rem;
}
-.card {
- background-color: var(--color-surface);
- border-radius: var(--border-radius);
- padding: var(--spacing-medium);
-}
-
-.stats {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: var(--spacing-medium);
+.compactStats {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 12px;
}
.statItem {
+ flex: 1;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
- padding: var(--spacing-medium);
- background-color: var(--color-background);
+ padding: 12px 8px;
+ background-color: var(--color-surface);
border-radius: var(--border-radius);
}
.statValue {
- font-size: 1.5rem;
+ font-size: 18px;
font-weight: bold;
color: var(--color-primary);
}
.statLabel {
- margin-top: 0.25rem;
+ margin-top: 4px;
color: var(--color-text);
opacity: 0.6;
- font-size: 0.875rem;
+ font-size: 0.75rem;
+}
+
+.tokenPacks {
+ margin-top: 12px;
+}
+
+@media (min-width: 768px) {
+ .compactStats {
+ gap: 12px;
+ }
+
+ .statItem {
+ padding: 16px;
+ }
}
diff --git a/src/screens/Profile.tsx b/src/screens/Profile.tsx
index 5565bcc..c6b76d8 100644
--- a/src/screens/Profile.tsx
+++ b/src/screens/Profile.tsx
@@ -1,9 +1,11 @@
import React, { useState, useEffect } from 'react';
import styles from './Profile.module.css';
-import { MOCK_USER } from '../constants/mock';
import { stickerService } from '../services/stickerService';
import apiService from '../services/api';
import { getCurrentUserId } from '../constants/user';
+import TokenPacksList from '../components/tokens/TokenPacksList';
+import { tokenPacks } from '../constants/tokenPacks';
+import { paymentService } from '../services/paymentService';
const Profile: React.FC = () => {
const [stickersCount, setStickersCount] = useState
(0);
@@ -32,37 +34,42 @@ const Profile: React.FC = () => {
fetchData();
}, []);
+ const handleBuyPack = (packId: string) => {
+ const pack = tokenPacks.find(p => p.id === packId);
+ if (!pack) return;
+
+ paymentService.showBuyTokensPopup(pack, () => {
+ // Обновляем данные после успешной оплаты
+ window.location.reload();
+ });
+ };
+
return (
-
- Профиль
-
-
- Ваша статистика и настройки
-
+
Профиль
+
Ваша статистика и настройки
-
-
-
- {MOCK_USER.balance}
- Токенов
-
-
- {loading ? '...' : stickersCount}
- Стикеров создано
-
-
- {loading ? '...' : packsCount}
- Стикерпаков
-
-
- 0
- Избранных
-
+
+
+ {loading ? '...' : stickersCount}
+ Стикеров создано
+
+
+ {loading ? '...' : packsCount}
+ Стикерпаков
+
+
+ 0
+ Избранных
+
+
);
};
diff --git a/src/services/api.ts b/src/services/api.ts
index b9b7ba1..6964680 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -48,7 +48,36 @@ class GenerationError extends Error {
}
}
+// Временное решение для работы с балансом токенов
+// В будущем будет заменено на API-запросы
+let mockBalance = -5; // Начальное значение из MOCK_USER
+
const apiService = {
+ // Метод для списания токенов
+ async deductTokens(userId: number, amount: number): Promise
{
+ try {
+ // В будущем здесь будет API-запрос для списания токенов
+ // Пока реализуем локальное списание
+ mockBalance -= amount;
+ return true;
+ } catch (error) {
+ console.error('Error deducting tokens:', error);
+ return false;
+ }
+ },
+
+ // Метод для получения текущего баланса
+ async getBalance(userId: number): Promise {
+ try {
+ // В будущем здесь будет API-запрос для получения баланса
+ // Пока возвращаем локальное значение
+ return mockBalance;
+ } catch (error) {
+ console.error('Error getting balance:', error);
+ throw error;
+ }
+ },
+
// Получение списка задач пользователя в статусе PENDING
async getUserPendingTasks(userId = getCurrentUserId()): Promise {
try {
diff --git a/src/services/paymentService.ts b/src/services/paymentService.ts
new file mode 100644
index 0000000..7a98c99
--- /dev/null
+++ b/src/services/paymentService.ts
@@ -0,0 +1,39 @@
+import { TokenPack } from '../constants/tokenPacks';
+
+export const paymentService = {
+ showBuyTokensPopup: (pack: TokenPack, onSuccess?: () => void) => {
+ // Проверяем наличие Telegram WebApp
+ if (!window.Telegram?.WebApp) {
+ console.error('Telegram WebApp не доступен');
+ return;
+ }
+
+ const webApp = window.Telegram.WebApp;
+
+ // Открываем окно оплаты Telegram
+ webApp.showPopup({
+ title: 'Покупка токенов',
+ message: `Вы собираетесь купить пакет "${pack.title}" за ${pack.price} Stars (${pack.priceRub} ₽)`,
+ buttons: [
+ {
+ type: 'ok',
+ text: 'Купить',
+ id: 'buy'
+ },
+ {
+ type: 'cancel',
+ text: 'Отмена'
+ }
+ ]
+ }, (buttonId: string) => {
+ if (buttonId === 'buy') {
+ // Открываем встроенный платеж Telegram
+ webApp.openInvoice(`sticker_tokens_${pack.id}`, (status: 'paid' | 'cancelled' | 'failed' | 'pending') => {
+ if (status === 'paid' && onSuccess) {
+ onSuccess();
+ }
+ });
+ }
+ });
+ }
+};
diff --git a/src/types/telegram-webapp.d.ts b/src/types/telegram-webapp.d.ts
index e810716..8a96da3 100644
--- a/src/types/telegram-webapp.d.ts
+++ b/src/types/telegram-webapp.d.ts
@@ -15,6 +15,18 @@ interface TelegramWebAppInitData {
hash?: string;
}
+interface PopupButton {
+ id?: string;
+ type: 'ok' | 'close' | 'cancel';
+ text?: string;
+}
+
+interface PopupParams {
+ title?: string;
+ message: string;
+ buttons?: PopupButton[];
+}
+
interface TelegramWebApp {
initData: string;
initDataUnsafe: TelegramWebAppInitData;
@@ -40,6 +52,8 @@ interface TelegramWebApp {
openTelegramLink(url: string): void;
showAlert(message: string, callback?: () => void): void;
showConfirm(message: string, callback?: (confirmed: boolean) => void): void;
+ showPopup(params: PopupParams, callback?: (buttonId: string) => void): void;
+ openInvoice(url: string, callback?: (status: 'paid' | 'cancelled' | 'failed' | 'pending') => void): void;
MainButton: {
text: string;
color: string;