diff --git a/app/components/MainApp.tsx b/app/components/MainApp.tsx index 8fa1865..717e73f 100644 --- a/app/components/MainApp.tsx +++ b/app/components/MainApp.tsx @@ -1,102 +1,65 @@ 'use client'; -import React, { useEffect, useState } from 'react'; -import { Container, Tabs, TabList, TabPanels, Tab, TabPanel, useToast, Spinner, Center } from '@chakra-ui/react'; +import React, { useState, useEffect } from 'react'; +import { Box, VStack, Spinner, Center, useToast } from '@chakra-ui/react'; import { UserProfile } from './UserProfile'; import { Shop } from './Shop'; import { TransferBalance } from './TransferBalance'; -import * as api from '../utils/api'; +import { auth, getProfile, getShopItems, purchaseItem, transferBalance } from '../utils/api'; import { IUser } from '../../backend/models/User'; import { IShopItem } from '../../backend/models/ShopItem'; +import { isDemoMode, getDemoWebApp } from '../utils/demo'; type SafeUser = Omit; -export function MainApp() { +export default function MainApp() { + const [isLoading, setIsLoading] = useState(true); const [user, setUser] = useState(null); const [shopItems, setShopItems] = useState([]); - const [isLoading, setIsLoading] = useState(true); const toast = useToast(); - useEffect(() => { - const initApp = async () => { - try { - setIsLoading(true); - // Динамически импортируем SDK только на клиенте + const initApp = async () => { + try { + setIsLoading(true); + + let webApp; + if (isDemoMode()) { + webApp = getDemoWebApp(); + } else { const WebApp = (await import('@twa-dev/sdk')).default; - - const initData = WebApp.initData; - if (!initData) { - throw new Error('Приложение должно быть открыто в Telegram'); - } - - // Авторизуем пользователя - const authData = await api.auth( - WebApp.initDataUnsafe.user?.id.toString() || '', - WebApp.initDataUnsafe.user?.username || '' - ); - setUser(authData.user); - - // Загружаем предметы магазина - const items = await api.getShopItems(); - setShopItems(items); - } catch (error: any) { - toast({ - title: 'Ошибка инициализации', - description: error.message, - status: 'error', - duration: 5000, - isClosable: true, - }); - } finally { - setIsLoading(false); + webApp = WebApp; } - }; + // Авторизация пользователя + const { user: telegramUser } = webApp.initDataUnsafe; + const authResponse = await auth(telegramUser.id.toString(), telegramUser.username || 'anonymous'); + + // Получение данных пользователя и магазина + const [profileData, shopData] = await Promise.all([ + getProfile(), + getShopItems() + ]); + + setUser(profileData as SafeUser); + setShopItems(shopData); + } catch (error: any) { + console.error('Initialization error:', error); + toast({ + title: 'Ошибка инициализации', + description: error.message || 'Произошла ошибка при загрузке приложения', + status: 'error', + duration: 5000, + isClosable: true, + }); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { initApp(); }, []); - const handlePurchase = async (itemId: string) => { - try { - const result = await api.purchaseItem(itemId); - setUser(result.user); - toast({ - title: 'Покупка успешна!', - status: 'success', - duration: 3000, - isClosable: true, - }); - } catch (error: any) { - toast({ - title: 'Ошибка покупки', - description: error.response?.data?.error || 'Произошла ошибка', - status: 'error', - duration: 3000, - isClosable: true, - }); - } - }; - - const handleTransfer = async (recipientUsername: string, amount: number) => { - try { - const result = await api.transferBalance(recipientUsername, amount); - setUser(prev => prev ? { ...prev, balance: result.balance } : null); - toast({ - title: 'Перевод выполнен', - status: 'success', - duration: 3000, - isClosable: true, - }); - } catch (error: any) { - toast({ - title: 'Ошибка перевода', - description: error.response?.data?.error || 'Произошла ошибка', - status: 'error', - duration: 3000, - isClosable: true, - }); - } - }; - if (isLoading) { return (
@@ -106,45 +69,73 @@ export function MainApp() { } if (!user) { - return null; + return ( +
+ + Пожалуйста, авторизуйтесь через Telegram или используйте демо-режим + +
+ ); } return ( - - - - Профиль - Магазин - Перевод - - - - - - - - - - - - - - - - - + + + { + try { + const response = await purchaseItem(item._id.toString()); + setUser(response.user as SafeUser); + toast({ + title: 'Успешная покупка', + description: `Вы приобрели ${item.name}`, + status: 'success', + duration: 3000, + isClosable: true, + }); + } catch (error: any) { + toast({ + title: 'Ошибка покупки', + description: error.message || 'Произошла ошибка при покупке', + status: 'error', + duration: 3000, + isClosable: true, + }); + } + }} + /> + { + try { + const response = await transferBalance(username, amount); + setUser(prev => ({ ...prev!, balance: response.balance } as SafeUser)); + toast({ + title: 'Успешный перевод', + description: `Вы перевели ${amount} монет пользователю ${username}`, + status: 'success', + duration: 3000, + isClosable: true, + }); + } catch (error: any) { + toast({ + title: 'Ошибка перевода', + description: error.message || 'Произошла ошибка при переводе', + status: 'error', + duration: 3000, + isClosable: true, + }); + } + }} + /> + ); } \ No newline at end of file diff --git a/app/components/Shop.tsx b/app/components/Shop.tsx index f5b33ba..c234350 100644 --- a/app/components/Shop.tsx +++ b/app/components/Shop.tsx @@ -1,34 +1,23 @@ 'use client'; import React from 'react'; -import { - Box, - Grid, - Text, - Button, - Image, - VStack, - useToast, - useColorModeValue, -} from '@chakra-ui/react'; +import { Box, SimpleGrid, Button, Text, Image, useToast } from '@chakra-ui/react'; import { IShopItem } from '../../backend/models/ShopItem'; interface ShopProps { items: IShopItem[]; userBalance: number; - onPurchase: (itemId: string) => Promise; + onPurchase: (item: IShopItem) => Promise; } -export const Shop: React.FC = ({ items, userBalance, onPurchase }) => { +export function Shop({ items, userBalance, onPurchase }: ShopProps) { const toast = useToast(); - const bgColor = useColorModeValue('white', 'gray.800'); - const borderColor = useColorModeValue('gray.200', 'gray.700'); const handlePurchase = async (item: IShopItem) => { if (userBalance < item.price) { toast({ title: 'Недостаточно средств', - description: 'У вас недостаточно Campfire монет для покупки этого предмета', + description: `Для покупки ${item.name} нужно ${item.price} монет`, status: 'error', duration: 3000, isClosable: true, @@ -36,74 +25,46 @@ export const Shop: React.FC = ({ items, userBalance, onPurchase }) => return; } - try { - await onPurchase(item.id); - toast({ - title: 'Покупка успешна!', - description: `Вы приобрели ${item.name}`, - status: 'success', - duration: 3000, - isClosable: true, - }); - } catch (error) { - toast({ - title: 'Ошибка при покупке', - description: 'Произошла ошибка при совершении покупки', - status: 'error', - duration: 3000, - isClosable: true, - }); - } + await onPurchase(item); }; return ( - - - Магазин - - - Ваш баланс: {userBalance} 🔥 - - - + + Магазин + Ваш баланс: {userBalance} монет + {items.map((item) => ( - - {item.imageUrl && ( - {item.name} - )} - {item.name} - - {item.description} - - - {item.price} 🔥 - - - + {item.imageUrl && ( + {item.name} + )} + {item.name} + {item.description} + {item.price} монет + ))} - + ); -}; \ No newline at end of file +} \ No newline at end of file diff --git a/app/utils/demo.ts b/app/utils/demo.ts new file mode 100644 index 0000000..4030711 --- /dev/null +++ b/app/utils/demo.ts @@ -0,0 +1,39 @@ +// Демо данные для тестирования без Telegram +const demoUser = { + id: 'demo_user', + first_name: 'Demo', + username: 'demo_user', + language_code: 'ru' +}; + +export const isDemoMode = () => { + return typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('demo'); +}; + +export const getDemoWebApp = () => { + return { + initData: 'demo_mode', + initDataUnsafe: { + user: demoUser, + start_param: 'demo' + }, + platform: 'demo', + colorScheme: 'light', + themeParams: { + bg_color: '#ffffff', + text_color: '#000000', + hint_color: '#999999', + link_color: '#2481cc', + button_color: '#2481cc', + button_text_color: '#ffffff' + }, + isExpanded: true, + viewportHeight: window.innerHeight, + viewportStableHeight: window.innerHeight, + headerColor: '#ffffff', + backgroundColor: '#ffffff', + ready: () => {}, + expand: () => {}, + close: () => {} + }; +}; \ No newline at end of file diff --git a/backend/models/User.ts b/backend/models/User.ts index d66192b..353477f 100644 --- a/backend/models/User.ts +++ b/backend/models/User.ts @@ -8,11 +8,11 @@ export interface IAchievement { } export interface IInventoryItem { - itemId: string; + itemId: mongoose.Types.ObjectId; name: string; description: string; quantity: number; - imageUrl?: string; + imageUrl: string; } export interface IUser extends Document { @@ -23,6 +23,7 @@ export interface IUser extends Document { balance: number; achievements: IAchievement[]; inventory: IInventoryItem[]; + addAchievement: (achievement: IAchievement) => Promise; createdAt: Date; updatedAt: Date; } @@ -40,7 +41,7 @@ const UserSchema: Schema = new Schema({ dateUnlocked: { type: Date, default: Date.now } }], inventory: [{ - itemId: String, + itemId: { type: Schema.Types.ObjectId, ref: 'ShopItem' }, name: String, description: String, quantity: Number, @@ -74,9 +75,7 @@ UserSchema.methods.addAchievement = async function(achievement: IAchievement) { // Награда за достижение this.balance += 50; await this.save(); - return true; } - return false; }; export default mongoose.model('User', UserSchema); \ No newline at end of file