fix: доработаны экраны оплаты
This commit is contained in:
parent
527226b1fb
commit
6c0fa84660
@ -1,26 +1,55 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { MOCK_USER } from '../../constants/mock';
|
||||
import { getUserInfo, isTelegramWebAppAvailable } from '../../constants/user';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { getUserInfo, isTelegramWebAppAvailable, getCurrentUserId } from '../../constants/user';
|
||||
import { images } from '../../assets';
|
||||
import apiService from '../../services/api';
|
||||
import TokenPacksModal from '../tokens/TokenPacksModal';
|
||||
import { paymentService } from '../../services/paymentService';
|
||||
import { tokenPacks } from '../../constants/tokenPacks';
|
||||
import styles from './Header.module.css';
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const [user, setUser] = useState(MOCK_USER);
|
||||
const navigate = useNavigate();
|
||||
const [user, setUser] = useState({
|
||||
telegramId: 0,
|
||||
username: '',
|
||||
avatarUrl: '',
|
||||
balance: 0
|
||||
});
|
||||
const [showTokensModal, setShowTokensModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Получаем информацию о пользователе
|
||||
const userInfo = getUserInfo();
|
||||
|
||||
// Если есть данные из Telegram, обновляем состояние
|
||||
if (isTelegramWebAppAvailable()) {
|
||||
setUser({
|
||||
telegramId: userInfo.id,
|
||||
username: userInfo.first_name + (userInfo.last_name ? ` ${userInfo.last_name}` : ''),
|
||||
avatarUrl: userInfo.photo_url || MOCK_USER.avatarUrl, // Используем фото из Telegram или дефолтное
|
||||
balance: MOCK_USER.balance // Баланс оставляем из моковых данных
|
||||
});
|
||||
}
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Получаем баланс пользователя
|
||||
const balance = await apiService.getBalance(getCurrentUserId());
|
||||
|
||||
// Если есть данные из Telegram, обновляем состояние
|
||||
if (isTelegramWebAppAvailable()) {
|
||||
setUser({
|
||||
telegramId: userInfo.id,
|
||||
username: userInfo.first_name + (userInfo.last_name ? ` ${userInfo.last_name}` : ''),
|
||||
avatarUrl: userInfo.photo_url || '/ava.jpg', // Используем фото из Telegram или дефолтное
|
||||
balance: balance
|
||||
});
|
||||
} else {
|
||||
// Для локальной разработки
|
||||
setUser({
|
||||
telegramId: 12345678,
|
||||
username: "TestUser",
|
||||
avatarUrl: "/ava.jpg",
|
||||
balance: balance
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching user data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@ -48,7 +77,7 @@ const Header: React.FC = () => {
|
||||
{/* Баланс токенов */}
|
||||
<button
|
||||
className={styles.balance}
|
||||
onClick={() => alert('Пополнить баланс')}
|
||||
onClick={() => setShowTokensModal(true)}
|
||||
title="Нажмите чтобы пополнить баланс"
|
||||
>
|
||||
<span className={styles.balanceIcon}>
|
||||
@ -60,6 +89,20 @@ const Header: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Модальное окно с пакетами токенов */}
|
||||
<TokenPacksModal
|
||||
isVisible={showTokensModal}
|
||||
onClose={() => setShowTokensModal(false)}
|
||||
onShowAllPacks={() => navigate('/profile')}
|
||||
missingTokens={0}
|
||||
onBuyPack={(packId) => {
|
||||
setShowTokensModal(false);
|
||||
paymentService.showBuyTokensPopup(tokenPacks.find(p => p.id === packId)!, () => {
|
||||
window.location.reload();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
.layout {
|
||||
min-height: 100vh;
|
||||
background-color: var(--color-background);
|
||||
padding-bottom: 4rem; /* Отступ для навигации */
|
||||
padding-bottom: 6rem; /* Увеличенный отступ для навигации */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative; /* Добавляем относительное позиционирование */
|
||||
height: 100%; /* Устанавливаем высоту 100% */
|
||||
overscroll-behavior: none; /* Предотвращает "резиновый" эффект на iOS */
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.main {
|
||||
@ -20,7 +20,7 @@
|
||||
-webkit-overflow-scrolling: touch; /* Улучшаем инерционный скроллинг на iOS */
|
||||
padding-top: 0px; /* Уменьшаем отступ до 20px */
|
||||
overscroll-behavior: contain; /* Ограничивает эффект скролла только этим элементом */
|
||||
height: calc(100% - 4rem); /* Вычитаем высоту навигации */
|
||||
height: calc(100% - 6rem); /* Вычитаем увеличенную высоту навигации */
|
||||
}
|
||||
|
||||
.container {
|
||||
|
||||
182
src/components/tokens/TokenPackCard.module.css
Normal file
182
src/components/tokens/TokenPackCard.module.css
Normal file
@ -0,0 +1,182 @@
|
||||
.card {
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background-color: var(--color-surface);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.popular {
|
||||
border: 2px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.bestValue {
|
||||
border: 2px solid var(--color-gold, #FFD700);
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: 10px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.popularBadge {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.bestValueBadge {
|
||||
background-color: var(--color-gold, #FFD700);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tokenInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.tokenIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.tokenCount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.baseTokens {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bonusTokens {
|
||||
color: var(--color-success, #4CAF50);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stickersCount {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--color-text);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.priceSection {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.buyButton {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
|
||||
.card.compact {
|
||||
padding: 8px;
|
||||
width: 150px; /* Adjust as needed */
|
||||
height: 250px; /* Adjust as needed */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start; /* Align items to the top */
|
||||
}
|
||||
|
||||
.card.compact .title {
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
text-align: center; /* Center the title */
|
||||
}
|
||||
|
||||
.card.compact .tokenInfo {
|
||||
margin-bottom: 4px;
|
||||
justify-content: center; /* Center the token info */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card.compact .tokenIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.card.compact .tokenCount {
|
||||
flex-direction: column; /* Stack tokens and bonus tokens */
|
||||
align-items: center; /* Center the items */
|
||||
}
|
||||
|
||||
.card.compact .baseTokens {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.card.compact .bonusTokens {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card.compact .stickersCount {
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
text-align: center; /* Center the stickers count */
|
||||
}
|
||||
|
||||
.card.compact .description {
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
text-align: center; /* Center the description */
|
||||
}
|
||||
|
||||
.card.compact .priceSection {
|
||||
flex-direction: column;
|
||||
align-items: center; /* Center the price section */
|
||||
margin-top: auto; /* Push price section to the bottom */
|
||||
}
|
||||
|
||||
.card.compact .price {
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
text-align: center; /* Center the price */
|
||||
}
|
||||
|
||||
.card.compact .buyButton {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
width: 100%; /* Make the button full width */
|
||||
}
|
||||
75
src/components/tokens/TokenPackCard.tsx
Normal file
75
src/components/tokens/TokenPackCard.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { images } from '../../assets';
|
||||
import { TokenPack } from '../../constants/tokenPacks';
|
||||
import styles from './TokenPackCard.module.css';
|
||||
|
||||
interface TokenPackCardProps extends TokenPack {
|
||||
onBuy: () => void;
|
||||
className?: string;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const TokenPackCard: React.FC<TokenPackCardProps> = ({
|
||||
title,
|
||||
tokens,
|
||||
bonusTokens,
|
||||
stickersCount,
|
||||
price,
|
||||
priceRub,
|
||||
description,
|
||||
isPopular,
|
||||
isBestValue,
|
||||
onBuy,
|
||||
className = ''
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`${styles.card} ${isPopular ? styles.popular : ''} ${isBestValue ? styles.bestValue : ''} ${className}`}
|
||||
>
|
||||
{isPopular && (
|
||||
<div className={`${styles.badge} ${styles.popularBadge}`}>
|
||||
Популярный выбор
|
||||
</div>
|
||||
)}
|
||||
{isBestValue && (
|
||||
<div className={`${styles.badge} ${styles.bestValueBadge}`}>
|
||||
Максимальная выгода
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 className={styles.title}>{title}</h3>
|
||||
|
||||
<div className={styles.tokenInfo}>
|
||||
<img src={images.tokenIcon} alt="Токены" className={styles.tokenIcon} />
|
||||
<div className={styles.tokenCount}>
|
||||
<span className={styles.baseTokens}>{tokens}</span>
|
||||
{bonusTokens > 0 && (
|
||||
<span className={styles.bonusTokens}>+{bonusTokens} БОНУС</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.stickersCount}>
|
||||
{stickersCount} стикеров
|
||||
</div>
|
||||
|
||||
{description && (
|
||||
<p className={styles.description}>{description}</p>
|
||||
)}
|
||||
|
||||
<div className={styles.priceSection}>
|
||||
<div className={styles.price}>
|
||||
{price} Stars ({priceRub} ₽)
|
||||
</div>
|
||||
<button
|
||||
className={styles.buyButton}
|
||||
onClick={onBuy}
|
||||
>
|
||||
КУПИТЬ
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenPackCard;
|
||||
111
src/components/tokens/TokenPacksList.module.css
Normal file
111
src/components/tokens/TokenPacksList.module.css
Normal file
@ -0,0 +1,111 @@
|
||||
.container {
|
||||
padding: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.horizontalScroll {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
padding: 8px 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-snap-type: x mandatory;
|
||||
gap: 16px;
|
||||
margin: 0 -8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.horizontalScroll > * {
|
||||
flex-shrink: 0;
|
||||
width: 280px;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
/* Стилизация скроллбара */
|
||||
.horizontalScroll::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.horizontalScroll::-webkit-scrollbar-track {
|
||||
background: var(--color-surface-variant);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.horizontalScroll::-webkit-scrollbar-thumb {
|
||||
background: var(--color-primary);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.horizontalScroll::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.horizontalScroll {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
padding: 8px 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-snap-type: x mandatory;
|
||||
gap: 16px;
|
||||
margin: 0 -8px;
|
||||
padding: 8px;
|
||||
flex-direction: row; /* Override column direction */
|
||||
scroll-margin-left: 150px; /* Adjust as needed */
|
||||
}
|
||||
|
||||
.horizontalScroll > *:first-child {
|
||||
margin-left: -150px; /* Adjust as needed */
|
||||
}
|
||||
|
||||
.list.compact {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.compactList {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Убираем margin у карточек внутри списка, так как используем gap */
|
||||
.list > * {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Адаптивный дизайн для больших экранов */
|
||||
@media (min-width: 768px) {
|
||||
.list:not(.horizontalScroll) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Анимация появления */
|
||||
.list > * {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
51
src/components/tokens/TokenPacksList.tsx
Normal file
51
src/components/tokens/TokenPacksList.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { tokenPacks } from '../../constants/tokenPacks';
|
||||
import TokenPackCard from './TokenPackCard';
|
||||
import styles from './TokenPacksList.module.css';
|
||||
|
||||
interface TokenPacksListProps {
|
||||
onBuyPack: (packId: string) => void;
|
||||
className?: string;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const TokenPacksList: React.FC<TokenPacksListProps> = ({
|
||||
onBuyPack,
|
||||
className = '',
|
||||
compact
|
||||
}) => {
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (compact && listRef.current) {
|
||||
const secondCard = listRef.current.children[1] as HTMLElement;
|
||||
if (secondCard) {
|
||||
listRef.current.scrollTo({
|
||||
left: secondCard.offsetLeft,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [compact]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.container} ${className}`}>
|
||||
<h2 className={styles.title}>Пакеты токенов</h2>
|
||||
<div
|
||||
className={`${styles.list} ${compact ? styles.horizontalScroll : ''}`}
|
||||
ref={listRef}
|
||||
>
|
||||
{tokenPacks.map(pack => (
|
||||
<TokenPackCard
|
||||
key={pack.id}
|
||||
{...pack}
|
||||
compact={compact}
|
||||
onBuy={() => onBuyPack(pack.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenPacksList;
|
||||
123
src/components/tokens/TokenPacksModal.module.css
Normal file
123
src/components/tokens/TokenPacksModal.module.css
Normal file
@ -0,0 +1,123 @@
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: var(--color-background);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-medium);
|
||||
width: calc(100% - var(--spacing-medium) * 2);
|
||||
max-width: 400px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
margin: var(--spacing-medium);
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
/* Стили только для iOS устройств */
|
||||
.modal {
|
||||
width: calc(100% - var(--spacing-large) * 2);
|
||||
margin: var(--spacing-large);
|
||||
max-width: 90vw; /* Ограничиваем ширину относительно вьюпорта */
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 16px 0 8px 0;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.horizontalScroll {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
padding: 8px 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-snap-type: x mandatory;
|
||||
gap: 16px;
|
||||
margin: 0 -8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.horizontalScroll > * {
|
||||
flex-shrink: 0;
|
||||
width: 280px;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.showAllButton {
|
||||
display: block;
|
||||
margin: 16px auto 0;
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.showAllButton:hover {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-secondary);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Стилизация скроллбара */
|
||||
.horizontalScroll::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.horizontalScroll::-webkit-scrollbar-track {
|
||||
background: var(--color-surface-variant);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.horizontalScroll::-webkit-scrollbar-thumb {
|
||||
background: var(--color-primary);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.horizontalScroll::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
70
src/components/tokens/TokenPacksModal.tsx
Normal file
70
src/components/tokens/TokenPacksModal.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { tokenPacks, TokenPack } from '../../constants/tokenPacks';
|
||||
import styles from './TokenPacksModal.module.css';
|
||||
import TokenPacksList from './TokenPacksList';
|
||||
|
||||
interface TokenPacksModalProps {
|
||||
isVisible: boolean;
|
||||
onClose: () => void;
|
||||
onShowAllPacks: () => void;
|
||||
missingTokens: number;
|
||||
onBuyPack: (packId: string) => void;
|
||||
}
|
||||
|
||||
const TokenPacksModal: React.FC<TokenPacksModalProps> = ({
|
||||
isVisible,
|
||||
onClose,
|
||||
onShowAllPacks,
|
||||
missingTokens,
|
||||
onBuyPack
|
||||
}) => {
|
||||
if (!isVisible) return null;
|
||||
|
||||
// Находим минимальный пакет, который покроет недостающие токены
|
||||
const recommendedPack = tokenPacks.find(pack =>
|
||||
(pack.tokens + pack.bonusTokens) >= missingTokens
|
||||
) || tokenPacks[0];
|
||||
|
||||
// Отображаем рекомендованный пакет и следующие два
|
||||
const startIndex = Math.max(
|
||||
0,
|
||||
tokenPacks.findIndex(pack => pack.id === recommendedPack.id)
|
||||
);
|
||||
const displayPacks = tokenPacks.slice(startIndex, startIndex + 3);
|
||||
|
||||
return (
|
||||
<div className={styles.overlay} onClick={onClose}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<h2 className={styles.title}>Недостаточно токенов для генерации</h2>
|
||||
|
||||
<TokenPacksList
|
||||
onBuyPack={onBuyPack}
|
||||
compact={true}
|
||||
/>
|
||||
|
||||
<button
|
||||
className={styles.showAllButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
onShowAllPacks();
|
||||
}}
|
||||
>
|
||||
Показать все пакеты
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={styles.closeButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenPacksModal;
|
||||
@ -4,7 +4,7 @@ export const MOCK_USER: MockUser = {
|
||||
telegramId: 12345678,
|
||||
username: "TestUser",
|
||||
avatarUrl: "/ava.jpg",
|
||||
balance: 1000
|
||||
balance: -5
|
||||
};
|
||||
|
||||
export const MOCK_STYLES: StyleOption[] = [
|
||||
|
||||
69
src/constants/tokenPacks.ts
Normal file
69
src/constants/tokenPacks.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { images } from '../assets';
|
||||
|
||||
export interface TokenPack {
|
||||
id: string;
|
||||
title: string;
|
||||
tokens: number;
|
||||
bonusTokens: number;
|
||||
stickersCount: number;
|
||||
price: number;
|
||||
priceRub: number;
|
||||
description: string;
|
||||
isPopular?: boolean;
|
||||
isBestValue?: boolean;
|
||||
}
|
||||
|
||||
export const tokenPacks: TokenPack[] = [
|
||||
{
|
||||
id: 'basic',
|
||||
title: 'Стартовый набор стикеромана',
|
||||
tokens: 150,
|
||||
bonusTokens: 0,
|
||||
stickersCount: 15,
|
||||
price: 100,
|
||||
priceRub: 179,
|
||||
description: 'Идеальный вариант для начала! Сгенерируйте 15 уникальных стикеров для вашего Telegram.'
|
||||
},
|
||||
{
|
||||
id: 'optimal',
|
||||
title: 'Стикерный запас',
|
||||
tokens: 250,
|
||||
bonusTokens: 30,
|
||||
stickersCount: 28,
|
||||
price: 150,
|
||||
priceRub: 259,
|
||||
description: 'Создавайте стикеры для всех случаев жизни с оптимальным запасом токенов.',
|
||||
isPopular: true
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
title: 'Стикерный энтузиаст',
|
||||
tokens: 500,
|
||||
bonusTokens: 75,
|
||||
stickersCount: 57,
|
||||
price: 350,
|
||||
priceRub: 589,
|
||||
description: 'Расширьте свои возможности! Создавайте стикеры без ограничений.'
|
||||
},
|
||||
{
|
||||
id: 'super',
|
||||
title: 'Стикерный магнат',
|
||||
tokens: 750,
|
||||
bonusTokens: 150,
|
||||
stickersCount: 90,
|
||||
price: 500,
|
||||
priceRub: 839,
|
||||
description: 'Для самых требовательных. Создавайте целые коллекции стикеров с максимальной выгодой.'
|
||||
},
|
||||
{
|
||||
id: 'unlimited',
|
||||
title: 'Бог стикеров',
|
||||
tokens: 1500,
|
||||
bonusTokens: 450,
|
||||
stickersCount: 195,
|
||||
price: 1000,
|
||||
priceRub: 1659,
|
||||
description: 'Для профессионалов и настоящих ценителей. Неограниченные возможности для творчества с максимальной выгодой.',
|
||||
isBestValue: true
|
||||
}
|
||||
];
|
||||
@ -2,60 +2,84 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-large);
|
||||
padding: calc(3rem + var(--spacing-small)) var(--spacing-medium) var(--spacing-large);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: var(--spacing-medium) 0;
|
||||
padding: var(--spacing-small) 0;
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-text);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 0.25rem;
|
||||
color: var(--color-text);
|
||||
opacity: 0.6;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.uploadArea {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--spacing-medium);
|
||||
background-color: var(--color-surface);
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.uploadBox {
|
||||
padding: var(--spacing-large);
|
||||
border: 2px dashed var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-small);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
padding: var(--spacing-large);
|
||||
border: 2px dashed var(--color-primary);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-surface);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.uploadBox:hover {
|
||||
border-color: var(--color-primary);
|
||||
background-color: var(--color-background);
|
||||
background-color: var(--color-surface-variant);
|
||||
border-color: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.uploadIcon {
|
||||
font-size: 3rem;
|
||||
font-size: 48px;
|
||||
margin-bottom: var(--spacing-small);
|
||||
}
|
||||
|
||||
.uploadText {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.uploadHint {
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tokenCost {
|
||||
margin-top: var(--spacing-medium);
|
||||
padding: var(--spacing-small) var(--spacing-medium);
|
||||
background-color: var(--color-surface-variant);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-small);
|
||||
}
|
||||
|
||||
@ -1,7 +1,44 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styles from './CreateSticker.module.css';
|
||||
import TokenPacksModal from '../components/tokens/TokenPacksModal';
|
||||
import { tokenPacks } from '../constants/tokenPacks';
|
||||
import { paymentService } from '../services/paymentService';
|
||||
import apiService from '../services/api';
|
||||
import { getCurrentUserId } from '../constants/user';
|
||||
|
||||
const TOKENS_PER_GENERATION = 10;
|
||||
|
||||
const CreateSticker: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [showTokensModal, setShowTokensModal] = useState(false);
|
||||
const [missingTokens, setMissingTokens] = useState(0);
|
||||
|
||||
const checkTokensAndProceed = async () => {
|
||||
try {
|
||||
const userTokens = await apiService.getBalance(getCurrentUserId());
|
||||
if (userTokens < TOKENS_PER_GENERATION) {
|
||||
setMissingTokens(TOKENS_PER_GENERATION - userTokens);
|
||||
setShowTokensModal(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error checking balance:', error);
|
||||
alert('Произошла ошибка при проверке баланса. Попробуйте позже.');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadClick = async () => {
|
||||
const canProceed = await checkTokensAndProceed();
|
||||
if (canProceed) {
|
||||
// Здесь будет логика загрузки фото
|
||||
console.log('Загрузка фото...');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
@ -13,7 +50,10 @@ const CreateSticker: React.FC = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.uploadArea}>
|
||||
<div
|
||||
className={styles.uploadArea}
|
||||
onClick={handleUploadClick}
|
||||
>
|
||||
<div className={styles.uploadBox}>
|
||||
<span className={styles.uploadIcon}>📷</span>
|
||||
<span className={styles.uploadText}>
|
||||
@ -22,8 +62,24 @@ const CreateSticker: React.FC = () => {
|
||||
<span className={styles.uploadHint}>
|
||||
или перетащите файл сюда
|
||||
</span>
|
||||
<span className={styles.tokenCost}>
|
||||
Стоимость: {TOKENS_PER_GENERATION} токенов
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TokenPacksModal
|
||||
isVisible={showTokensModal}
|
||||
onClose={() => setShowTokensModal(false)}
|
||||
onShowAllPacks={() => navigate('/profile')}
|
||||
missingTokens={missingTokens}
|
||||
onBuyPack={(packId: string) => {
|
||||
setShowTokensModal(false);
|
||||
paymentService.showBuyTokensPopup(tokenPacks.find(p => p.id === packId)!, () => {
|
||||
window.location.reload();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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<LastGenerationData>({});
|
||||
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 = () => {
|
||||
/>
|
||||
|
||||
<div className={styles.content}>
|
||||
{/* Модальное окно с пакетами токенов */}
|
||||
<TokenPacksModal
|
||||
isVisible={showTokensModal}
|
||||
onClose={() => setShowTokensModal(false)}
|
||||
onShowAllPacks={() => navigate('/profile')}
|
||||
missingTokens={missingTokens}
|
||||
onBuyPack={(packId: string) => {
|
||||
setShowTokensModal(false);
|
||||
paymentService.showBuyTokensPopup(tokenPacks.find(p => p.id === packId)!, () => {
|
||||
window.location.reload();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/* Блоки из конфигурации */}
|
||||
<div className={styles.blocks}>
|
||||
{homeScreenConfig.homeScreen.blocks
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<number>(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 (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h1 className={styles.title}>
|
||||
Профиль
|
||||
</h1>
|
||||
<p className={styles.subtitle}>
|
||||
Ваша статистика и настройки
|
||||
</p>
|
||||
<h1 className={styles.title}>Профиль</h1>
|
||||
<p className={styles.subtitle}>Ваша статистика и настройки</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.card}>
|
||||
<div className={styles.stats}>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statValue}>{MOCK_USER.balance}</span>
|
||||
<span className={styles.statLabel}>Токенов</span>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statValue}>{loading ? '...' : stickersCount}</span>
|
||||
<span className={styles.statLabel}>Стикеров создано</span>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statValue}>{loading ? '...' : packsCount}</span>
|
||||
<span className={styles.statLabel}>Стикерпаков</span>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statValue}>0</span>
|
||||
<span className={styles.statLabel}>Избранных</span>
|
||||
</div>
|
||||
<div className={styles.compactStats}>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statValue}>{loading ? '...' : stickersCount}</span>
|
||||
<span className={styles.statLabel}>Стикеров создано</span>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statValue}>{loading ? '...' : packsCount}</span>
|
||||
<span className={styles.statLabel}>Стикерпаков</span>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statValue}>0</span>
|
||||
<span className={styles.statLabel}>Избранных</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TokenPacksList
|
||||
onBuyPack={handleBuyPack}
|
||||
className={styles.tokenPacks}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -48,7 +48,36 @@ class GenerationError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// Временное решение для работы с балансом токенов
|
||||
// В будущем будет заменено на API-запросы
|
||||
let mockBalance = -5; // Начальное значение из MOCK_USER
|
||||
|
||||
const apiService = {
|
||||
// Метод для списания токенов
|
||||
async deductTokens(userId: number, amount: number): Promise<boolean> {
|
||||
try {
|
||||
// В будущем здесь будет API-запрос для списания токенов
|
||||
// Пока реализуем локальное списание
|
||||
mockBalance -= amount;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error deducting tokens:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Метод для получения текущего баланса
|
||||
async getBalance(userId: number): Promise<number> {
|
||||
try {
|
||||
// В будущем здесь будет API-запрос для получения баланса
|
||||
// Пока возвращаем локальное значение
|
||||
return mockBalance;
|
||||
} catch (error) {
|
||||
console.error('Error getting balance:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Получение списка задач пользователя в статусе PENDING
|
||||
async getUserPendingTasks(userId = getCurrentUserId()): Promise<PendingTask[]> {
|
||||
try {
|
||||
|
||||
39
src/services/paymentService.ts
Normal file
39
src/services/paymentService.ts
Normal file
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
14
src/types/telegram-webapp.d.ts
vendored
14
src/types/telegram-webapp.d.ts
vendored
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user