Compare commits
2 Commits
2b86322b75
...
c4328b7698
Author | SHA1 | Date | |
---|---|---|---|
c4328b7698 | |||
2af270e3d8 |
@ -1,48 +1,52 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Container, Tabs, TabList, TabPanels, Tab, TabPanel, useToast, Spinner, Center } from '@chakra-ui/react';
|
import { Box, VStack, Spinner, Center, useToast } from '@chakra-ui/react';
|
||||||
import { UserProfile } from './UserProfile';
|
import { UserProfile } from './UserProfile';
|
||||||
import { Shop } from './Shop';
|
import { Shop } from './Shop';
|
||||||
import { TransferBalance } from './TransferBalance';
|
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 { IUser } from '../../backend/models/User';
|
||||||
import { IShopItem } from '../../backend/models/ShopItem';
|
import { IShopItem } from '../../backend/models/ShopItem';
|
||||||
|
import { isDemoMode, getDemoWebApp } from '../utils/demo';
|
||||||
|
|
||||||
type SafeUser = Omit<IUser, keyof Document>;
|
type SafeUser = Omit<IUser, keyof Document>;
|
||||||
|
|
||||||
export function MainApp() {
|
export default function MainApp() {
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [user, setUser] = useState<SafeUser | null>(null);
|
const [user, setUser] = useState<SafeUser | null>(null);
|
||||||
const [shopItems, setShopItems] = useState<IShopItem[]>([]);
|
const [shopItems, setShopItems] = useState<IShopItem[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initApp = async () => {
|
const initApp = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
// Динамически импортируем SDK только на клиенте
|
|
||||||
const WebApp = (await import('@twa-dev/sdk')).default;
|
|
||||||
|
|
||||||
const initData = WebApp.initData;
|
let webApp;
|
||||||
if (!initData) {
|
if (isDemoMode()) {
|
||||||
throw new Error('Приложение должно быть открыто в Telegram');
|
webApp = getDemoWebApp();
|
||||||
|
} else {
|
||||||
|
const WebApp = (await import('@twa-dev/sdk')).default;
|
||||||
|
webApp = WebApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Авторизуем пользователя
|
// Авторизация пользователя
|
||||||
const authData = await api.auth(
|
const { user: telegramUser } = webApp.initDataUnsafe;
|
||||||
WebApp.initDataUnsafe.user?.id.toString() || '',
|
const authResponse = await auth(telegramUser.id.toString(), telegramUser.username || 'anonymous');
|
||||||
WebApp.initDataUnsafe.user?.username || ''
|
|
||||||
);
|
|
||||||
setUser(authData.user);
|
|
||||||
|
|
||||||
// Загружаем предметы магазина
|
// Получение данных пользователя и магазина
|
||||||
const items = await api.getShopItems();
|
const [profileData, shopData] = await Promise.all([
|
||||||
setShopItems(items);
|
getProfile(),
|
||||||
|
getShopItems()
|
||||||
|
]);
|
||||||
|
|
||||||
|
setUser(profileData as SafeUser);
|
||||||
|
setShopItems(shopData);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.error('Initialization error:', error);
|
||||||
toast({
|
toast({
|
||||||
title: 'Ошибка инициализации',
|
title: 'Ошибка инициализации',
|
||||||
description: error.message,
|
description: error.message || 'Произошла ошибка при загрузке приложения',
|
||||||
status: 'error',
|
status: 'error',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
@ -52,51 +56,10 @@ export function MainApp() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
initApp();
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Center h="100vh">
|
<Center h="100vh">
|
||||||
@ -106,20 +69,17 @@ export function MainApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return (
|
||||||
|
<Center h="100vh">
|
||||||
|
<Box p={4} textAlign="center">
|
||||||
|
Пожалуйста, авторизуйтесь через Telegram или используйте демо-режим
|
||||||
|
</Box>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxW="container.xl" py={8}>
|
<VStack spacing={6} p={4}>
|
||||||
<Tabs isFitted variant="enclosed">
|
|
||||||
<TabList mb="1em">
|
|
||||||
<Tab>Профиль</Tab>
|
|
||||||
<Tab>Магазин</Tab>
|
|
||||||
<Tab>Перевод</Tab>
|
|
||||||
</TabList>
|
|
||||||
|
|
||||||
<TabPanels>
|
|
||||||
<TabPanel>
|
|
||||||
<UserProfile
|
<UserProfile
|
||||||
username={user.username}
|
username={user.username}
|
||||||
level={user.level}
|
level={user.level}
|
||||||
@ -127,24 +87,55 @@ export function MainApp() {
|
|||||||
balance={user.balance}
|
balance={user.balance}
|
||||||
achievements={user.achievements}
|
achievements={user.achievements}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabPanel>
|
|
||||||
<Shop
|
<Shop
|
||||||
items={shopItems}
|
items={shopItems}
|
||||||
userBalance={user.balance}
|
userBalance={user.balance}
|
||||||
onPurchase={handlePurchase}
|
onPurchase={async (item: IShopItem) => {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabPanel>
|
|
||||||
<TransferBalance
|
<TransferBalance
|
||||||
userBalance={user.balance}
|
userBalance={user.balance}
|
||||||
onTransfer={handleTransfer}
|
onTransfer={async (username, amount) => {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</VStack>
|
||||||
</TabPanels>
|
|
||||||
</Tabs>
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,34 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { Box, SimpleGrid, Button, Text, Image, useToast } from '@chakra-ui/react';
|
||||||
Box,
|
|
||||||
Grid,
|
|
||||||
Text,
|
|
||||||
Button,
|
|
||||||
Image,
|
|
||||||
VStack,
|
|
||||||
useToast,
|
|
||||||
useColorModeValue,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { IShopItem } from '../../backend/models/ShopItem';
|
import { IShopItem } from '../../backend/models/ShopItem';
|
||||||
|
|
||||||
interface ShopProps {
|
interface ShopProps {
|
||||||
items: IShopItem[];
|
items: IShopItem[];
|
||||||
userBalance: number;
|
userBalance: number;
|
||||||
onPurchase: (itemId: string) => Promise<void>;
|
onPurchase: (item: IShopItem) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Shop: React.FC<ShopProps> = ({ items, userBalance, onPurchase }) => {
|
export function Shop({ items, userBalance, onPurchase }: ShopProps) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const bgColor = useColorModeValue('white', 'gray.800');
|
|
||||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
|
||||||
|
|
||||||
const handlePurchase = async (item: IShopItem) => {
|
const handlePurchase = async (item: IShopItem) => {
|
||||||
if (userBalance < item.price) {
|
if (userBalance < item.price) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Недостаточно средств',
|
title: 'Недостаточно средств',
|
||||||
description: 'У вас недостаточно Campfire монет для покупки этого предмета',
|
description: `Для покупки ${item.name} нужно ${item.price} монет`,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
@ -36,74 +25,46 @@ export const Shop: React.FC<ShopProps> = ({ items, userBalance, onPurchase }) =>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await onPurchase(item);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box p={4}>
|
<Box w="100%">
|
||||||
<Text fontSize="2xl" fontWeight="bold" mb={4}>
|
<Text fontSize="2xl" mb={4}>Магазин</Text>
|
||||||
Магазин
|
<Text mb={4}>Ваш баланс: {userBalance} монет</Text>
|
||||||
</Text>
|
<SimpleGrid columns={[1, 2, 3]} spacing={6}>
|
||||||
<Text mb={4}>
|
|
||||||
Ваш баланс: {userBalance} 🔥
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Grid templateColumns={['1fr', 'repeat(2, 1fr)', 'repeat(3, 1fr)']} gap={4}>
|
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<Box
|
<Box
|
||||||
key={item.id}
|
key={item._id.toString()}
|
||||||
p={4}
|
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderRadius="lg"
|
borderRadius="lg"
|
||||||
borderColor={borderColor}
|
overflow="hidden"
|
||||||
bg={bgColor}
|
p={4}
|
||||||
>
|
>
|
||||||
<VStack spacing={3}>
|
|
||||||
{item.imageUrl && (
|
{item.imageUrl && (
|
||||||
<Image
|
<Image
|
||||||
src={item.imageUrl}
|
src={item.imageUrl}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
boxSize="100px"
|
height="200px"
|
||||||
|
width="100%"
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
borderRadius="md"
|
mb={4}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Text fontWeight="bold">{item.name}</Text>
|
<Text fontSize="xl" mb={2}>{item.name}</Text>
|
||||||
<Text fontSize="sm" color="gray.500">
|
<Text mb={2}>{item.description}</Text>
|
||||||
{item.description}
|
<Text mb={4} color="green.500">{item.price} монет</Text>
|
||||||
</Text>
|
|
||||||
<Text color="green.500" fontWeight="bold">
|
|
||||||
{item.price} 🔥
|
|
||||||
</Text>
|
|
||||||
<Button
|
<Button
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
width="full"
|
|
||||||
onClick={() => handlePurchase(item)}
|
onClick={() => handlePurchase(item)}
|
||||||
isDisabled={userBalance < item.price}
|
isDisabled={userBalance < item.price}
|
||||||
|
w="100%"
|
||||||
>
|
>
|
||||||
Купить
|
Купить
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</SimpleGrid>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
}
|
39
app/utils/demo.ts
Normal file
39
app/utils/demo.ts
Normal file
@ -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: () => {}
|
||||||
|
};
|
||||||
|
};
|
@ -8,11 +8,11 @@ export interface IAchievement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IInventoryItem {
|
export interface IInventoryItem {
|
||||||
itemId: string;
|
itemId: mongoose.Types.ObjectId;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
imageUrl?: string;
|
imageUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUser extends Document {
|
export interface IUser extends Document {
|
||||||
@ -23,6 +23,7 @@ export interface IUser extends Document {
|
|||||||
balance: number;
|
balance: number;
|
||||||
achievements: IAchievement[];
|
achievements: IAchievement[];
|
||||||
inventory: IInventoryItem[];
|
inventory: IInventoryItem[];
|
||||||
|
addAchievement: (achievement: IAchievement) => Promise<void>;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@ -40,7 +41,7 @@ const UserSchema: Schema = new Schema({
|
|||||||
dateUnlocked: { type: Date, default: Date.now }
|
dateUnlocked: { type: Date, default: Date.now }
|
||||||
}],
|
}],
|
||||||
inventory: [{
|
inventory: [{
|
||||||
itemId: String,
|
itemId: { type: Schema.Types.ObjectId, ref: 'ShopItem' },
|
||||||
name: String,
|
name: String,
|
||||||
description: String,
|
description: String,
|
||||||
quantity: Number,
|
quantity: Number,
|
||||||
@ -74,9 +75,7 @@ UserSchema.methods.addAchievement = async function(achievement: IAchievement) {
|
|||||||
// Награда за достижение
|
// Награда за достижение
|
||||||
this.balance += 50;
|
this.balance += 50;
|
||||||
await this.save();
|
await this.save();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default mongoose.model<IUser>('User', UserSchema);
|
export default mongoose.model<IUser>('User', UserSchema);
|
Loading…
Reference in New Issue
Block a user