Compare commits
No commits in common. "next" and "main" have entirely different histories.
23
.env
23
.env
@ -1,23 +0,0 @@
|
|||||||
# Настройки Next.js
|
|
||||||
NEXT_PUBLIC_URL=http://localhost:3000
|
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:3001/api
|
|
||||||
|
|
||||||
# Настройки сервера
|
|
||||||
PORT=3001
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
# База данных
|
|
||||||
MONGODB_URI=mongodb://localhost:27017/campfire-id
|
|
||||||
|
|
||||||
# Безопасность
|
|
||||||
JWT_SECRET=your-super-secret-key-change-in-production
|
|
||||||
|
|
||||||
# Telegram WebApp
|
|
||||||
NEXT_PUBLIC_BOT_USERNAME=CampFireIDBot
|
|
||||||
NEXT_PUBLIC_BOT_TOKEN=7641998551:AAF_2-av1rUy_-icQ1fsfHOyYBGFt8cEmE4
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL=https://t.me/CampFireIDBot/app
|
|
||||||
|
|
||||||
# Telegram Bot
|
|
||||||
TELEGRAM_BOT_TOKEN=7641998551:AAF_2-av1rUy_-icQ1fsfHOyYBGFt8cEmE4
|
|
||||||
TELEGRAM_APP_ID=your_app_id_here
|
|
||||||
TELEGRAM_APP_HASH=your_app_hash_here
|
|
14
.env.example
14
.env.example
@ -1,14 +0,0 @@
|
|||||||
# Telegram Bot
|
|
||||||
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
|
||||||
TELEGRAM_APP_ID=your_app_id_here
|
|
||||||
TELEGRAM_APP_HASH=your_app_hash_here
|
|
||||||
|
|
||||||
# MongoDB
|
|
||||||
MONGODB_URI=mongodb://localhost:27017/campfire_id
|
|
||||||
|
|
||||||
# JWT
|
|
||||||
JWT_SECRET=your_jwt_secret_here
|
|
||||||
|
|
||||||
# App
|
|
||||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
||||||
NODE_ENV=development
|
|
@ -1,21 +0,0 @@
|
|||||||
# Настройки Next.js
|
|
||||||
NEXT_PUBLIC_URL=https://kubek.campfiregg.ru
|
|
||||||
NEXT_PUBLIC_API_URL=https://kubek.campfiregg.ru/api
|
|
||||||
|
|
||||||
# Настройки сервера
|
|
||||||
PORT=3000
|
|
||||||
NODE_ENV=production
|
|
||||||
|
|
||||||
# База данных
|
|
||||||
MONGODB_URI=mongodb://localhost:27017/campfire-id
|
|
||||||
|
|
||||||
# Безопасность
|
|
||||||
JWT_SECRET=your-super-secret-key-change-in-production
|
|
||||||
|
|
||||||
# Telegram WebApp
|
|
||||||
NEXT_PUBLIC_BOT_USERNAME=CampFireIDBot
|
|
||||||
NEXT_PUBLIC_BOT_TOKEN=7641998551:AAF_2-av1rUy_-icQ1fsfHOyYBGFt8cEmE4
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL=https://t.me/CampFireIDBot/app
|
|
||||||
|
|
||||||
# Telegram Bot
|
|
||||||
TELEGRAM_BOT_TOKEN=7641998551:AAF_2-av1rUy_-icQ1fsfHOyYBGFt8cEmE4
|
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,7 +1,2 @@
|
|||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
/node_modules
|
/node_modules
|
||||||
/.next
|
|
||||||
/.next/static
|
|
||||||
/.next/cache
|
|
||||||
/.next/server
|
|
||||||
/.next
|
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"pages": {
|
|
||||||
"/layout": [
|
|
||||||
"static/chunks/webpack.js",
|
|
||||||
"static/chunks/main-app.js",
|
|
||||||
"static/css/app/layout.css",
|
|
||||||
"static/chunks/app/layout.js"
|
|
||||||
],
|
|
||||||
"/page": [
|
|
||||||
"static/chunks/webpack.js",
|
|
||||||
"static/chunks/main-app.js",
|
|
||||||
"static/chunks/app/page.js"
|
|
||||||
],
|
|
||||||
"/not-found": [
|
|
||||||
"static/chunks/webpack.js",
|
|
||||||
"static/chunks/main-app.js",
|
|
||||||
"static/chunks/app/not-found.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"polyfillFiles": [
|
|
||||||
"static/chunks/polyfills.js"
|
|
||||||
],
|
|
||||||
"devFiles": [],
|
|
||||||
"ampDevFiles": [],
|
|
||||||
"lowPriorityFiles": [
|
|
||||||
"static/development/_buildManifest.js",
|
|
||||||
"static/development/_ssgManifest.js"
|
|
||||||
],
|
|
||||||
"rootMainFiles": [
|
|
||||||
"static/chunks/webpack.js",
|
|
||||||
"static/chunks/main-app.js"
|
|
||||||
],
|
|
||||||
"pages": {
|
|
||||||
"/_app": []
|
|
||||||
},
|
|
||||||
"ampFirstPages": []
|
|
||||||
}
|
|
BIN
.next/cache/webpack/client-development/0.pack.gz
vendored
BIN
.next/cache/webpack/client-development/0.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/client-development/1.pack.gz
vendored
BIN
.next/cache/webpack/client-development/1.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/client-development/index.pack.gz
vendored
BIN
.next/cache/webpack/client-development/index.pack.gz
vendored
Binary file not shown.
Binary file not shown.
BIN
.next/cache/webpack/server-development/0.pack.gz
vendored
BIN
.next/cache/webpack/server-development/0.pack.gz
vendored
Binary file not shown.
BIN
.next/cache/webpack/server-development/index.pack.gz
vendored
BIN
.next/cache/webpack/server-development/index.pack.gz
vendored
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
{"type": "commonjs"}
|
|
@ -1 +0,0 @@
|
|||||||
{}
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"/not-found": "app/not-found.js",
|
|
||||||
"/page": "app/page.js"
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
self.__BUILD_MANIFEST={"polyfillFiles":["static/chunks/polyfills.js"],"devFiles":[],"ampDevFiles":[],"lowPriorityFiles":["static/development/_buildManifest.js","static/development/_ssgManifest.js"],"rootMainFiles":["static/chunks/webpack.js","static/chunks/main-app.js"],"pages":{"/_app":[]},"ampFirstPages":[]}
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"sortedMiddleware": [],
|
|
||||||
"middleware": {},
|
|
||||||
"functions": {},
|
|
||||||
"version": 2
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
self.__REACT_LOADABLE_MANIFEST="{}"
|
|
@ -1 +0,0 @@
|
|||||||
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"
|
|
@ -1 +0,0 @@
|
|||||||
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}
|
|
@ -1 +0,0 @@
|
|||||||
{}
|
|
@ -1 +0,0 @@
|
|||||||
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {}\n}"
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"node": {},
|
|
||||||
"edge": {}
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
|||||||
self.__BUILD_MANIFEST = {__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},sortedPages:["\u002F_app"]};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()
|
|
@ -1 +0,0 @@
|
|||||||
self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
|
|
@ -1 +0,0 @@
|
|||||||
{"type": "module"}
|
|
@ -1,49 +0,0 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
import { connectDB } from '../utils/db';
|
|
||||||
import User from '../models/User';
|
|
||||||
import { verifyTelegramWebAppData } from '../utils/telegram';
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
if (req.method !== 'GET') {
|
|
||||||
return res.status(405).json({ message: 'Method not allowed' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Проверяем данные Telegram WebApp
|
|
||||||
const initData = req.headers['x-telegram-init-data'] as string;
|
|
||||||
if (!initData) {
|
|
||||||
return res.status(401).json({ message: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const telegramData = await verifyTelegramWebAppData(initData);
|
|
||||||
if (!telegramData) {
|
|
||||||
return res.status(401).json({ message: 'Invalid Telegram data' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Подключаемся к базе данных
|
|
||||||
await connectDB();
|
|
||||||
|
|
||||||
// Получаем или создаем пользователя
|
|
||||||
let user = await User.findOne({ telegramId: telegramData.user.id });
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
user = await User.create({
|
|
||||||
telegramId: telegramData.user.id,
|
|
||||||
username: telegramData.user.username || `user${telegramData.user.id}`,
|
|
||||||
level: 1,
|
|
||||||
experience: 0,
|
|
||||||
balance: 1000, // Начальный баланс для новых пользователей
|
|
||||||
achievements: [],
|
|
||||||
inventory: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json(user);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in user API:', error);
|
|
||||||
res.status(500).json({ message: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Center, Spinner } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
export default function ClientOnly({ children }: { children: React.ReactNode }) {
|
|
||||||
const [hasMounted, setHasMounted] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setHasMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!hasMounted) {
|
|
||||||
return (
|
|
||||||
<Center h="100vh">
|
|
||||||
<Spinner size="xl" />
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import { Center, Spinner } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
const MainApp = dynamic(() => import('./MainApp'), {
|
|
||||||
ssr: false,
|
|
||||||
loading: () => (
|
|
||||||
<Center h="100vh">
|
|
||||||
<Spinner size="xl" />
|
|
||||||
</Center>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function ClientPage() {
|
|
||||||
return <MainApp />;
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Center, Spinner, Text, VStack } from '@chakra-ui/react';
|
|
||||||
import { useTelegramWebApp } from '../hooks/useTelegramWebApp';
|
|
||||||
|
|
||||||
export default function MainApp() {
|
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
|
||||||
const { webApp, error, isLoading } = useTelegramWebApp();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!isMounted || isLoading) {
|
|
||||||
return (
|
|
||||||
<Center h="100vh">
|
|
||||||
<Spinner size="xl" />
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<Center h="100vh">
|
|
||||||
<VStack>
|
|
||||||
<Text color="red.500">Ошибка инициализации</Text>
|
|
||||||
<Text>{error.message}</Text>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!webApp) {
|
|
||||||
return (
|
|
||||||
<Center h="100vh">
|
|
||||||
<Text>WebApp не инициализирован</Text>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Center h="100vh">
|
|
||||||
<Text>Добро пожаловать, {webApp.initDataUnsafe.user?.first_name}!</Text>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React from '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: (item: IShopItem) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Shop({ items, userBalance, onPurchase }: ShopProps) {
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const handlePurchase = async (item: IShopItem) => {
|
|
||||||
if (userBalance < item.price) {
|
|
||||||
toast({
|
|
||||||
title: 'Недостаточно средств',
|
|
||||||
description: `Для покупки ${item.name} нужно ${item.price} монет`,
|
|
||||||
status: 'error',
|
|
||||||
duration: 3000,
|
|
||||||
isClosable: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await onPurchase(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box w="100%">
|
|
||||||
<Text fontSize="2xl" mb={4}>Магазин</Text>
|
|
||||||
<Text mb={4}>Ваш баланс: {userBalance} монет</Text>
|
|
||||||
<SimpleGrid columns={[1, 2, 3]} spacing={6}>
|
|
||||||
{items.map((item) => (
|
|
||||||
<Box
|
|
||||||
key={item._id.toString()}
|
|
||||||
borderWidth="1px"
|
|
||||||
borderRadius="lg"
|
|
||||||
overflow="hidden"
|
|
||||||
p={4}
|
|
||||||
>
|
|
||||||
{item.imageUrl && (
|
|
||||||
<Image
|
|
||||||
src={item.imageUrl}
|
|
||||||
alt={item.name}
|
|
||||||
height="200px"
|
|
||||||
width="100%"
|
|
||||||
objectFit="cover"
|
|
||||||
mb={4}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Text fontSize="xl" mb={2}>{item.name}</Text>
|
|
||||||
<Text mb={2}>{item.description}</Text>
|
|
||||||
<Text mb={4} color="green.500">{item.price} монет</Text>
|
|
||||||
<Button
|
|
||||||
colorScheme="blue"
|
|
||||||
onClick={() => handlePurchase(item)}
|
|
||||||
isDisabled={userBalance < item.price}
|
|
||||||
w="100%"
|
|
||||||
>
|
|
||||||
Купить
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
VStack,
|
|
||||||
Text,
|
|
||||||
Input,
|
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
NumberInput,
|
|
||||||
NumberInputField,
|
|
||||||
NumberInputStepper,
|
|
||||||
NumberIncrementStepper,
|
|
||||||
NumberDecrementStepper,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
|
|
||||||
interface TransferBalanceProps {
|
|
||||||
userBalance: number;
|
|
||||||
onTransfer: (username: string, amount: number) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TransferBalance({ userBalance, onTransfer }: TransferBalanceProps) {
|
|
||||||
const [username, setUsername] = useState('');
|
|
||||||
const [amount, setAmount] = useState(0);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleTransfer = async () => {
|
|
||||||
if (!username || amount <= 0 || amount > userBalance) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
await onTransfer(username, amount);
|
|
||||||
// Очищаем форму после успешного перевода
|
|
||||||
setUsername('');
|
|
||||||
setAmount(0);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box w="100%" p={4} borderWidth="1px" borderRadius="lg">
|
|
||||||
<VStack spacing={4} align="stretch">
|
|
||||||
<Text fontSize="2xl">Перевод монет</Text>
|
|
||||||
<Text>Ваш баланс: {userBalance} монет</Text>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>Имя пользователя получателя</FormLabel>
|
|
||||||
<Input
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
placeholder="Введите имя пользователя"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>Сумма перевода</FormLabel>
|
|
||||||
<NumberInput
|
|
||||||
value={amount}
|
|
||||||
onChange={(_, value) => setAmount(value)}
|
|
||||||
min={1}
|
|
||||||
max={userBalance}
|
|
||||||
>
|
|
||||||
<NumberInputField />
|
|
||||||
<NumberInputStepper>
|
|
||||||
<NumberIncrementStepper />
|
|
||||||
<NumberDecrementStepper />
|
|
||||||
</NumberInputStepper>
|
|
||||||
</NumberInput>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
colorScheme="blue"
|
|
||||||
onClick={handleTransfer}
|
|
||||||
isLoading={isLoading}
|
|
||||||
isDisabled={!username || amount <= 0 || amount > userBalance}
|
|
||||||
>
|
|
||||||
Перевести
|
|
||||||
</Button>
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { Box, VStack, Text, Progress, SimpleGrid, Badge } from '@chakra-ui/react';
|
|
||||||
import { IAchievement } from '../../backend/models/User';
|
|
||||||
|
|
||||||
interface UserProfileProps {
|
|
||||||
username: string;
|
|
||||||
level: number;
|
|
||||||
experience: number;
|
|
||||||
balance: number;
|
|
||||||
achievements: IAchievement[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UserProfile({ username, level, experience, balance, achievements }: UserProfileProps) {
|
|
||||||
// Простая формула для определения прогресса опыта
|
|
||||||
const expNeeded = level * 100;
|
|
||||||
const expProgress = (experience / expNeeded) * 100;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box w="100%" p={4} borderWidth="1px" borderRadius="lg">
|
|
||||||
<VStack spacing={4} align="stretch">
|
|
||||||
<Text fontSize="2xl">Профиль</Text>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Text fontSize="lg">Имя пользователя: {username}</Text>
|
|
||||||
<Text>Уровень: {level}</Text>
|
|
||||||
<Text mb={2}>Опыт: {experience}/{expNeeded}</Text>
|
|
||||||
<Progress value={expProgress} colorScheme="green" />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Text fontSize="lg">Баланс: {balance} монет</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{achievements.length > 0 && (
|
|
||||||
<Box>
|
|
||||||
<Text fontSize="lg" mb={2}>Достижения:</Text>
|
|
||||||
<SimpleGrid columns={[1, 2, 3]} spacing={4}>
|
|
||||||
{achievements.map((achievement) => (
|
|
||||||
<Badge
|
|
||||||
key={achievement.id}
|
|
||||||
p={2}
|
|
||||||
borderRadius="md"
|
|
||||||
colorScheme="purple"
|
|
||||||
variant="subtle"
|
|
||||||
>
|
|
||||||
{achievement.name}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
|
||||||
import type { WebApp } from '@twa-dev/types';
|
|
||||||
import { isDemoMode, getDemoWebApp } from '../utils/demo';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
Telegram?: {
|
|
||||||
WebApp?: WebApp;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseTelegramWebAppResult {
|
|
||||||
webApp: WebApp | null;
|
|
||||||
error: Error | null;
|
|
||||||
isLoading: boolean;
|
|
||||||
isInitialized: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTelegramWebApp(): UseTelegramWebAppResult {
|
|
||||||
const [webApp, setWebApp] = useState<WebApp | null>(null);
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
|
||||||
|
|
||||||
const initWebApp = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
if (isDemoMode()) {
|
|
||||||
const demoWebApp = getDemoWebApp();
|
|
||||||
setWebApp(demoWebApp);
|
|
||||||
setIsInitialized(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
if (window.Telegram?.WebApp) {
|
|
||||||
setWebApp(window.Telegram.WebApp);
|
|
||||||
setIsInitialized(true);
|
|
||||||
} else {
|
|
||||||
throw new Error('Telegram WebApp не найден');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка инициализации WebApp:', err);
|
|
||||||
setError(err instanceof Error ? err : new Error('Неизвестная ошибка'));
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
initWebApp();
|
|
||||||
}, [initWebApp]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!webApp) return;
|
|
||||||
|
|
||||||
const handleThemeChange = () => {
|
|
||||||
// Обновляем состояние при изменении темы
|
|
||||||
setWebApp(prevWebApp => {
|
|
||||||
if (!prevWebApp) return null;
|
|
||||||
return { ...prevWebApp };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
webApp.onEvent('themeChanged', handleThemeChange);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
webApp.offEvent('themeChanged', handleThemeChange);
|
|
||||||
};
|
|
||||||
}, [webApp]);
|
|
||||||
|
|
||||||
return { webApp, error, isLoading, isInitialized };
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
import type { Metadata } from 'next';
|
|
||||||
import { Providers } from './providers';
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'CampFire ID',
|
|
||||||
description: 'Telegram Mini App for user achievements and virtual economy',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<html lang="ru">
|
|
||||||
<body>
|
|
||||||
<Providers>
|
|
||||||
{children}
|
|
||||||
</Providers>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IUser extends Document {
|
|
||||||
telegramId: number;
|
|
||||||
username: string;
|
|
||||||
level: number;
|
|
||||||
experience: number;
|
|
||||||
balance: number;
|
|
||||||
achievements: Array<{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
unlockedAt: Date;
|
|
||||||
}>;
|
|
||||||
inventory: Array<{
|
|
||||||
itemId: string;
|
|
||||||
quantity: number;
|
|
||||||
}>;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserSchema: Schema = new Schema({
|
|
||||||
telegramId: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
username: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
level: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
experience: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
balance: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
achievements: [{
|
|
||||||
id: String,
|
|
||||||
name: String,
|
|
||||||
description: String,
|
|
||||||
unlockedAt: {
|
|
||||||
type: Date,
|
|
||||||
default: Date.now,
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
inventory: [{
|
|
||||||
itemId: String,
|
|
||||||
quantity: Number,
|
|
||||||
}],
|
|
||||||
}, {
|
|
||||||
timestamps: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Виртуальное поле для расчета следующего уровня
|
|
||||||
UserSchema.virtual('nextLevelExp').get(function() {
|
|
||||||
return Math.floor(100 * Math.pow(1.5, this.level - 1));
|
|
||||||
});
|
|
||||||
|
|
||||||
export default mongoose.models.User || mongoose.model<IUser>('User', UserSchema);
|
|
@ -1,5 +0,0 @@
|
|||||||
import ClientPage from './components/ClientPage';
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return <ClientPage />;
|
|
||||||
}
|
|
@ -1,123 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Container,
|
|
||||||
Tab,
|
|
||||||
TabList,
|
|
||||||
TabPanel,
|
|
||||||
TabPanels,
|
|
||||||
Tabs,
|
|
||||||
useToast,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { WebApp } from '@telegram-mini-apps/sdk';
|
|
||||||
import UserProfile from '../components/UserProfile';
|
|
||||||
import Shop from '../components/Shop';
|
|
||||||
import TransferBalance from '../components/TransferBalance';
|
|
||||||
import { IUser } from '../models/User';
|
|
||||||
|
|
||||||
const Home: React.FC = () => {
|
|
||||||
const [user, setUser] = useState<IUser | null>(null);
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Инициализация Telegram Mini App
|
|
||||||
WebApp.ready();
|
|
||||||
|
|
||||||
// Загрузка данных пользователя
|
|
||||||
fetchUserData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchUserData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/user');
|
|
||||||
const data = await response.json();
|
|
||||||
setUser(data);
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
title: 'Ошибка',
|
|
||||||
description: 'Не удалось загрузить данные пользователя',
|
|
||||||
status: 'error',
|
|
||||||
duration: 3000,
|
|
||||||
isClosable: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePurchase = async (itemId: string, price: number) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/shop/purchase', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ itemId, price }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
await fetchUserData(); // Обновляем данные пользователя
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTransfer = async (recipientId: string, amount: number) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/transfer', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ recipientId, amount }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
await fetchUserData(); // Обновляем данные пользователя
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return null; // или компонент загрузки
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container maxW="container.md" py={4}>
|
|
||||||
<Tabs isFitted variant="enclosed">
|
|
||||||
<TabList mb="1em">
|
|
||||||
<Tab>Профиль</Tab>
|
|
||||||
<Tab>Магазин</Tab>
|
|
||||||
<Tab>Перевод</Tab>
|
|
||||||
</TabList>
|
|
||||||
|
|
||||||
<TabPanels>
|
|
||||||
<TabPanel>
|
|
||||||
<UserProfile user={user} />
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabPanel>
|
|
||||||
<Shop
|
|
||||||
userBalance={user.balance}
|
|
||||||
onPurchase={handlePurchase}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabPanel>
|
|
||||||
<TransferBalance
|
|
||||||
userBalance={user.balance}
|
|
||||||
onTransfer={handleTransfer}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
|
||||||
</Tabs>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Home;
|
|
@ -1,25 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { CacheProvider } from '@chakra-ui/next-js';
|
|
||||||
import { ChakraProvider, extendTheme } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
const theme = extendTheme({
|
|
||||||
config: {
|
|
||||||
initialColorMode: 'light',
|
|
||||||
useSystemColorMode: false,
|
|
||||||
},
|
|
||||||
fonts: {
|
|
||||||
heading: 'Inter, sans-serif',
|
|
||||||
body: 'Inter, sans-serif',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export function Providers({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<CacheProvider>
|
|
||||||
<ChakraProvider theme={theme}>
|
|
||||||
{children}
|
|
||||||
</ChakraProvider>
|
|
||||||
</CacheProvider>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
export interface Achievement {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
dateUnlocked: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
level: number;
|
|
||||||
experience: number;
|
|
||||||
balance: number;
|
|
||||||
achievements: Achievement[];
|
|
||||||
inventory: InventoryItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InventoryItem {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
quantity: number;
|
|
||||||
imageUrl?: string;
|
|
||||||
}
|
|
120
app/utils/api.ts
120
app/utils/api.ts
@ -1,120 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { isDemoMode } from './demo';
|
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api';
|
|
||||||
|
|
||||||
const api = axios.create({
|
|
||||||
baseURL: API_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Демо-данные
|
|
||||||
const demoData = {
|
|
||||||
user: {
|
|
||||||
_id: 'demo_user_id',
|
|
||||||
telegramId: '12345',
|
|
||||||
username: 'demo_user',
|
|
||||||
level: 1,
|
|
||||||
experience: 0,
|
|
||||||
balance: 1000,
|
|
||||||
achievements: [],
|
|
||||||
},
|
|
||||||
shopItems: [
|
|
||||||
{
|
|
||||||
_id: 'demo_item_1',
|
|
||||||
name: 'Демо предмет 1',
|
|
||||||
description: 'Описание демо предмета 1',
|
|
||||||
price: 100,
|
|
||||||
type: 'consumable',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: 'demo_item_2',
|
|
||||||
name: 'Демо предмет 2',
|
|
||||||
description: 'Описание демо предмета 2',
|
|
||||||
price: 200,
|
|
||||||
type: 'permanent',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Интерцептор для добавления токена
|
|
||||||
api.interceptors.request.use((config) => {
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Интерцептор для обработки ошибок
|
|
||||||
api.interceptors.response.use(
|
|
||||||
(response) => response,
|
|
||||||
(error) => {
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
localStorage.removeItem('token');
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const auth = async (telegramId: string, username: string) => {
|
|
||||||
if (isDemoMode()) {
|
|
||||||
localStorage.setItem('token', 'demo_token');
|
|
||||||
return { token: 'demo_token', user: demoData.user };
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api.post('/auth', { telegramId, username });
|
|
||||||
const { token } = response.data;
|
|
||||||
localStorage.setItem('token', token);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getProfile = async () => {
|
|
||||||
if (isDemoMode()) {
|
|
||||||
return demoData.user;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api.get('/profile');
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getShopItems = async () => {
|
|
||||||
if (isDemoMode()) {
|
|
||||||
return demoData.shopItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api.get('/shop');
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const purchaseItem = async (itemId: string) => {
|
|
||||||
if (isDemoMode()) {
|
|
||||||
const item = demoData.shopItems.find(item => item._id === itemId);
|
|
||||||
if (!item) {
|
|
||||||
throw new Error('Предмет не найден');
|
|
||||||
}
|
|
||||||
if (demoData.user.balance < item.price) {
|
|
||||||
throw new Error('Недостаточно средств');
|
|
||||||
}
|
|
||||||
demoData.user.balance -= item.price;
|
|
||||||
return { user: demoData.user };
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api.post(`/shop/purchase/${itemId}`);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const transferBalance = async (username: string, amount: number) => {
|
|
||||||
if (isDemoMode()) {
|
|
||||||
if (demoData.user.balance < amount) {
|
|
||||||
throw new Error('Недостаточно средств');
|
|
||||||
}
|
|
||||||
demoData.user.balance -= amount;
|
|
||||||
return { balance: demoData.user.balance };
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api.post('/transfer', { username, amount });
|
|
||||||
return response.data;
|
|
||||||
};
|
|
@ -1,38 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
|
|
||||||
const MONGODB_URI = process.env.MONGODB_URI!;
|
|
||||||
|
|
||||||
if (!MONGODB_URI) {
|
|
||||||
throw new Error('Please define the MONGODB_URI environment variable');
|
|
||||||
}
|
|
||||||
|
|
||||||
let cached = global.mongoose;
|
|
||||||
|
|
||||||
if (!cached) {
|
|
||||||
cached = global.mongoose = { conn: null, promise: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function connectDB() {
|
|
||||||
if (cached.conn) {
|
|
||||||
return cached.conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cached.promise) {
|
|
||||||
const opts = {
|
|
||||||
bufferCommands: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
|
|
||||||
return mongoose;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
cached.conn = await cached.promise;
|
|
||||||
} catch (e) {
|
|
||||||
cached.promise = null;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cached.conn;
|
|
||||||
}
|
|
@ -1,116 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { WebApp, WebAppUser, WebAppInitData, Platforms, HapticFeedback } from '@twa-dev/types';
|
|
||||||
|
|
||||||
// Демо данные для тестирования без Telegram
|
|
||||||
const demoUser: WebAppUser = {
|
|
||||||
id: 12345,
|
|
||||||
first_name: 'Demo',
|
|
||||||
username: 'demo_user',
|
|
||||||
language_code: 'ru',
|
|
||||||
is_premium: false,
|
|
||||||
last_name: '',
|
|
||||||
photo_url: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
const demoInitData: WebAppInitData = {
|
|
||||||
query_id: 'demo_query',
|
|
||||||
user: demoUser,
|
|
||||||
auth_date: Date.now(),
|
|
||||||
hash: 'demo_hash',
|
|
||||||
start_param: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isDemoMode = () => {
|
|
||||||
return typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('demo');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Создаем заглушки для методов WebApp
|
|
||||||
const createNoopFunction = () => () => {};
|
|
||||||
|
|
||||||
// Создаем заглушку для HapticFeedback
|
|
||||||
const demoHapticFeedback: HapticFeedback = {
|
|
||||||
impactOccurred: (_style: "light" | "medium" | "heavy" | "rigid" | "soft") => demoHapticFeedback,
|
|
||||||
notificationOccurred: (_type: "error" | "success" | "warning") => demoHapticFeedback,
|
|
||||||
selectionChanged: () => demoHapticFeedback
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDemoWebApp = (): WebApp => ({
|
|
||||||
initData: JSON.stringify(demoInitData),
|
|
||||||
initDataUnsafe: demoInitData,
|
|
||||||
platform: 'WEBVIEW' as Platforms,
|
|
||||||
colorScheme: 'light',
|
|
||||||
themeParams: {
|
|
||||||
bg_color: '#ffffff',
|
|
||||||
text_color: '#000000',
|
|
||||||
hint_color: '#999999',
|
|
||||||
link_color: '#2481cc',
|
|
||||||
button_color: '#2481cc',
|
|
||||||
button_text_color: '#ffffff',
|
|
||||||
secondary_bg_color: '#f0f0f0'
|
|
||||||
},
|
|
||||||
isExpanded: true,
|
|
||||||
viewportHeight: typeof window !== 'undefined' ? window.innerHeight : 800,
|
|
||||||
viewportStableHeight: typeof window !== 'undefined' ? window.innerHeight : 800,
|
|
||||||
headerColor: '#ffffff',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
isClosingConfirmationEnabled: true,
|
|
||||||
BackButton: {
|
|
||||||
isVisible: false,
|
|
||||||
onClick: createNoopFunction(),
|
|
||||||
offClick: createNoopFunction(),
|
|
||||||
show: createNoopFunction(),
|
|
||||||
hide: createNoopFunction()
|
|
||||||
},
|
|
||||||
MainButton: {
|
|
||||||
text: '',
|
|
||||||
color: '#2481cc',
|
|
||||||
textColor: '#ffffff',
|
|
||||||
isVisible: false,
|
|
||||||
isProgressVisible: false,
|
|
||||||
isActive: true,
|
|
||||||
setText: createNoopFunction(),
|
|
||||||
onClick: createNoopFunction(),
|
|
||||||
offClick: createNoopFunction(),
|
|
||||||
show: createNoopFunction(),
|
|
||||||
hide: createNoopFunction(),
|
|
||||||
enable: createNoopFunction(),
|
|
||||||
disable: createNoopFunction(),
|
|
||||||
showProgress: createNoopFunction(),
|
|
||||||
hideProgress: createNoopFunction(),
|
|
||||||
setParams: createNoopFunction()
|
|
||||||
},
|
|
||||||
HapticFeedback: demoHapticFeedback,
|
|
||||||
CloudStorage: {
|
|
||||||
setItem: createNoopFunction(),
|
|
||||||
getItem: createNoopFunction(),
|
|
||||||
getItems: createNoopFunction(),
|
|
||||||
removeItem: createNoopFunction(),
|
|
||||||
removeItems: createNoopFunction(),
|
|
||||||
getKeys: createNoopFunction()
|
|
||||||
},
|
|
||||||
version: '7.0',
|
|
||||||
isVersionAtLeast: (version: string) => true,
|
|
||||||
setHeaderColor: createNoopFunction(),
|
|
||||||
setBackgroundColor: createNoopFunction(),
|
|
||||||
enableClosingConfirmation: createNoopFunction(),
|
|
||||||
disableClosingConfirmation: createNoopFunction(),
|
|
||||||
onEvent: createNoopFunction(),
|
|
||||||
offEvent: createNoopFunction(),
|
|
||||||
sendData: createNoopFunction(),
|
|
||||||
switchInlineQuery: createNoopFunction(),
|
|
||||||
openLink: createNoopFunction(),
|
|
||||||
openTelegramLink: createNoopFunction(),
|
|
||||||
openInvoice: createNoopFunction(),
|
|
||||||
showPopup: createNoopFunction(),
|
|
||||||
showAlert: createNoopFunction(),
|
|
||||||
showConfirm: createNoopFunction(),
|
|
||||||
showScanQrPopup: createNoopFunction(),
|
|
||||||
closeScanQrPopup: createNoopFunction(),
|
|
||||||
readTextFromClipboard: createNoopFunction(),
|
|
||||||
requestWriteAccess: createNoopFunction(),
|
|
||||||
requestContact: createNoopFunction(),
|
|
||||||
ready: createNoopFunction(),
|
|
||||||
expand: createNoopFunction(),
|
|
||||||
close: createNoopFunction(),
|
|
||||||
});
|
|
@ -1,67 +0,0 @@
|
|||||||
import crypto from 'crypto';
|
|
||||||
|
|
||||||
interface TelegramUser {
|
|
||||||
id: number;
|
|
||||||
first_name: string;
|
|
||||||
last_name?: string;
|
|
||||||
username?: string;
|
|
||||||
language_code?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TelegramInitData {
|
|
||||||
query_id?: string;
|
|
||||||
user: TelegramUser;
|
|
||||||
auth_date: number;
|
|
||||||
hash: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function verifyTelegramWebAppData(initData: string): Promise<TelegramInitData | null> {
|
|
||||||
try {
|
|
||||||
const botToken = process.env.TELEGRAM_BOT_TOKEN;
|
|
||||||
if (!botToken) {
|
|
||||||
throw new Error('TELEGRAM_BOT_TOKEN is not defined');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Разбираем строку initData
|
|
||||||
const searchParams = new URLSearchParams(initData);
|
|
||||||
const hash = searchParams.get('hash');
|
|
||||||
searchParams.delete('hash');
|
|
||||||
|
|
||||||
// Сортируем параметры
|
|
||||||
const params = Array.from(searchParams.entries())
|
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
// Создаем секретный ключ
|
|
||||||
const secretKey = crypto
|
|
||||||
.createHmac('sha256', 'WebAppData')
|
|
||||||
.update(botToken)
|
|
||||||
.digest();
|
|
||||||
|
|
||||||
// Вычисляем хеш
|
|
||||||
const calculatedHash = crypto
|
|
||||||
.createHmac('sha256', secretKey)
|
|
||||||
.update(params)
|
|
||||||
.digest('hex');
|
|
||||||
|
|
||||||
// Проверяем хеш
|
|
||||||
if (calculatedHash !== hash) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Парсим данные пользователя
|
|
||||||
const user: TelegramUser = JSON.parse(searchParams.get('user') || '{}');
|
|
||||||
const authDate = parseInt(searchParams.get('auth_date') || '0', 10);
|
|
||||||
|
|
||||||
return {
|
|
||||||
query_id: searchParams.get('query_id') || undefined,
|
|
||||||
user,
|
|
||||||
auth_date: authDate,
|
|
||||||
hash: hash || '',
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error verifying Telegram data:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IShopItem extends Document {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
price: number;
|
|
||||||
imageUrl?: string;
|
|
||||||
available: boolean;
|
|
||||||
type: 'badge' | 'frame' | 'effect' | 'other';
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ShopItemSchema: Schema = new Schema({
|
|
||||||
name: { type: String, required: true },
|
|
||||||
description: { type: String, required: true },
|
|
||||||
price: { type: Number, required: true },
|
|
||||||
imageUrl: { type: String },
|
|
||||||
available: { type: Boolean, default: true },
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
enum: ['badge', 'frame', 'effect', 'other']
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
timestamps: true
|
|
||||||
});
|
|
||||||
|
|
||||||
export default mongoose.model<IShopItem>('ShopItem', ShopItemSchema);
|
|
@ -1,81 +0,0 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IAchievement {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
dateUnlocked: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IInventoryItem {
|
|
||||||
itemId: mongoose.Types.ObjectId;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
quantity: number;
|
|
||||||
imageUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IUser extends Document {
|
|
||||||
telegramId: string;
|
|
||||||
username: string;
|
|
||||||
level: number;
|
|
||||||
experience: number;
|
|
||||||
balance: number;
|
|
||||||
achievements: IAchievement[];
|
|
||||||
inventory: IInventoryItem[];
|
|
||||||
addAchievement: (achievement: IAchievement) => Promise<void>;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserSchema: Schema = new Schema({
|
|
||||||
telegramId: { type: String, required: true, unique: true },
|
|
||||||
username: { type: String, required: true },
|
|
||||||
level: { type: Number, default: 1 },
|
|
||||||
experience: { type: Number, default: 0 },
|
|
||||||
balance: { type: Number, default: 0 },
|
|
||||||
achievements: [{
|
|
||||||
id: String,
|
|
||||||
name: String,
|
|
||||||
description: String,
|
|
||||||
dateUnlocked: { type: Date, default: Date.now }
|
|
||||||
}],
|
|
||||||
inventory: [{
|
|
||||||
itemId: { type: Schema.Types.ObjectId, ref: 'ShopItem' },
|
|
||||||
name: String,
|
|
||||||
description: String,
|
|
||||||
quantity: Number,
|
|
||||||
imageUrl: String
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
timestamps: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Метод для добавления опыта и повышения уровня
|
|
||||||
UserSchema.methods.addExperience = async function(amount: number) {
|
|
||||||
this.experience += amount;
|
|
||||||
|
|
||||||
// Простая формула для определения необходимого опыта для следующего уровня
|
|
||||||
const expNeeded = this.level * 100;
|
|
||||||
|
|
||||||
while (this.experience >= expNeeded) {
|
|
||||||
this.experience -= expNeeded;
|
|
||||||
this.level += 1;
|
|
||||||
// Бонус за новый уровень
|
|
||||||
this.balance += this.level * 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.save();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Метод для добавления достижения
|
|
||||||
UserSchema.methods.addAchievement = async function(achievement: IAchievement) {
|
|
||||||
if (!this.achievements.some(a => a.id === achievement.id)) {
|
|
||||||
this.achievements.push(achievement);
|
|
||||||
// Награда за достижение
|
|
||||||
this.balance += 50;
|
|
||||||
await this.save();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default mongoose.model<IUser>('User', UserSchema);
|
|
@ -1,207 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import mongoose from 'mongoose';
|
|
||||||
import dotenv from 'dotenv';
|
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
import cors from 'cors';
|
|
||||||
import User, { IUser } from './models/User';
|
|
||||||
import ShopItem from './models/ShopItem';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
// Расширяем типы Express
|
|
||||||
declare global {
|
|
||||||
namespace Express {
|
|
||||||
interface Request {
|
|
||||||
user?: {
|
|
||||||
userId: string;
|
|
||||||
telegramId: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
// Middleware
|
|
||||||
app.use(cors({
|
|
||||||
origin: process.env.NEXT_PUBLIC_URL || 'http://localhost:3000',
|
|
||||||
credentials: true
|
|
||||||
}));
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
// Подключение к MongoDB
|
|
||||||
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/campfire-id')
|
|
||||||
.then(() => console.log('Connected to MongoDB'))
|
|
||||||
.catch(err => console.error('MongoDB connection error:', err));
|
|
||||||
|
|
||||||
// Middleware для проверки JWT
|
|
||||||
const authenticateToken = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
||||||
const authHeader = req.headers['authorization'];
|
|
||||||
const token = authHeader && authHeader.split(' ')[1];
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return res.status(401).json({ error: 'Требуется авторизация' });
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key', (err: any, user: any) => {
|
|
||||||
if (err) {
|
|
||||||
return res.status(403).json({ error: 'Недействительный токен' });
|
|
||||||
}
|
|
||||||
req.user = user;
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Обработка ошибок
|
|
||||||
const errorHandler = (err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
||||||
console.error(err.stack);
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Ошибка сервера',
|
|
||||||
message: process.env.NODE_ENV === 'development' ? err.message : undefined
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Регистрация/авторизация пользователя
|
|
||||||
app.post('/api/auth', async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const { telegramId, username } = req.body;
|
|
||||||
|
|
||||||
if (!telegramId || !username) {
|
|
||||||
return res.status(400).json({ error: 'Отсутствуют обязательные поля' });
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = await User.findOne({ telegramId });
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
user = new User({
|
|
||||||
telegramId,
|
|
||||||
username,
|
|
||||||
// Начальный бонус для новых пользователей
|
|
||||||
balance: 100
|
|
||||||
});
|
|
||||||
await user.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ userId: user._id, telegramId },
|
|
||||||
process.env.JWT_SECRET || 'your-secret-key',
|
|
||||||
{ expiresIn: '7d' }
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({ token, user });
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получение профиля пользователя
|
|
||||||
app.get('/api/profile', authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const user = await User.findById(req.user.userId);
|
|
||||||
if (!user) {
|
|
||||||
return res.status(404).json({ error: 'Пользователь не найден' });
|
|
||||||
}
|
|
||||||
res.json(user);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получение списка предметов в магазине
|
|
||||||
app.get('/api/shop', authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const items = await ShopItem.find({ available: true });
|
|
||||||
res.json(items);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Покупка предмета
|
|
||||||
app.post('/api/shop/purchase', authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { itemId } = req.body;
|
|
||||||
const user = await User.findById(req.user.userId);
|
|
||||||
const item = await ShopItem.findById(itemId);
|
|
||||||
|
|
||||||
if (!user || !item) {
|
|
||||||
return res.status(404).json({ error: 'Пользователь или предмет не найден' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.balance < item.price) {
|
|
||||||
return res.status(400).json({ error: 'Недостаточно средств' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем баланс и инвентарь
|
|
||||||
user.balance -= item.price;
|
|
||||||
const existingItem = user.inventory.find(i => i.itemId === itemId);
|
|
||||||
|
|
||||||
if (existingItem) {
|
|
||||||
existingItem.quantity += 1;
|
|
||||||
} else {
|
|
||||||
user.inventory.push({
|
|
||||||
itemId: item._id,
|
|
||||||
name: item.name,
|
|
||||||
description: item.description,
|
|
||||||
quantity: 1,
|
|
||||||
imageUrl: item.imageUrl
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await user.save();
|
|
||||||
res.json({ success: true, user });
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Перевод баланса между пользователями
|
|
||||||
app.post('/api/transfer', authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { recipientUsername, amount } = req.body;
|
|
||||||
|
|
||||||
if (amount <= 0) {
|
|
||||||
return res.status(400).json({ error: 'Неверная сумма перевода' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const sender = await User.findById(req.user.userId);
|
|
||||||
const recipient = await User.findOne({ username: recipientUsername });
|
|
||||||
|
|
||||||
if (!sender || !recipient) {
|
|
||||||
return res.status(404).json({ error: 'Отправитель или получатель не найден' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sender.balance < amount) {
|
|
||||||
return res.status(400).json({ error: 'Недостаточно средств' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Выполняем перевод
|
|
||||||
sender.balance -= amount;
|
|
||||||
recipient.balance += amount;
|
|
||||||
|
|
||||||
await sender.save();
|
|
||||||
await recipient.save();
|
|
||||||
|
|
||||||
// Добавляем достижение за первый перевод
|
|
||||||
if (sender.achievements.every(a => a.id !== 'first_transfer')) {
|
|
||||||
await sender.addAchievement({
|
|
||||||
id: 'first_transfer',
|
|
||||||
name: 'Щедрая душа',
|
|
||||||
description: 'Совершили первый перевод',
|
|
||||||
dateUnlocked: new Date()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ success: true, balance: sender.balance });
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем обработчик ошибок в конце
|
|
||||||
app.use(errorHandler);
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3001;
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`Server is running on port ${PORT}`);
|
|
||||||
});
|
|
16
env.d.ts
vendored
16
env.d.ts
vendored
@ -1,16 +0,0 @@
|
|||||||
declare namespace NodeJS {
|
|
||||||
interface ProcessEnv {
|
|
||||||
NEXT_PUBLIC_URL: string;
|
|
||||||
NEXT_PUBLIC_API_URL: string;
|
|
||||||
NEXT_PUBLIC_BOT_USERNAME: string;
|
|
||||||
NEXT_PUBLIC_BOT_TOKEN: string;
|
|
||||||
NEXT_PUBLIC_WEBAPP_URL: string;
|
|
||||||
PORT: string;
|
|
||||||
NODE_ENV: 'development' | 'production' | 'test';
|
|
||||||
MONGODB_URI: string;
|
|
||||||
JWT_SECRET: string;
|
|
||||||
TELEGRAM_BOT_TOKEN: string;
|
|
||||||
TELEGRAM_APP_ID: string;
|
|
||||||
TELEGRAM_APP_HASH: string;
|
|
||||||
}
|
|
||||||
}
|
|
5
next-env.d.ts
vendored
5
next-env.d.ts
vendored
@ -1,5 +0,0 @@
|
|||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
@ -1,48 +0,0 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {
|
|
||||||
reactStrictMode: true,
|
|
||||||
swcMinify: true,
|
|
||||||
output: 'standalone',
|
|
||||||
experimental: {
|
|
||||||
serverActions: {
|
|
||||||
allowedOrigins: ['telegram.org', 't.me', 'kubek.campfiregg.ru'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
webpack: (config) => {
|
|
||||||
config.externals.push({
|
|
||||||
'utf-8-validate': 'commonjs utf-8-validate',
|
|
||||||
'bufferutil': 'commonjs bufferutil',
|
|
||||||
});
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
images: {
|
|
||||||
domains: ['t.me', 'telegram.org', 'kubek.campfiregg.ru'],
|
|
||||||
},
|
|
||||||
async headers() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: '/:path*',
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: 'Access-Control-Allow-Origin',
|
|
||||||
value: '*',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Content-Security-Policy',
|
|
||||||
value: "default-src 'self' telegram.org *.telegram.org t.me kubek.campfiregg.ru data: 'unsafe-inline' 'unsafe-eval';"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
typescript: {
|
|
||||||
ignoreBuildErrors: true,
|
|
||||||
},
|
|
||||||
poweredByHeader: false,
|
|
||||||
compress: true,
|
|
||||||
productionBrowserSourceMaps: false,
|
|
||||||
basePath: '',
|
|
||||||
assetPrefix: process.env.NODE_ENV === 'production' ? 'https://kubek.campfiregg.ru' : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = nextConfig
|
|
6659
package-lock.json
generated
6659
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
59
package.json
59
package.json
@ -1,45 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "campfire-id",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
|
||||||
"description": "Telegram Mini App for user achievements and virtual economy",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint",
|
|
||||||
"prod:build": "NODE_ENV=production next build",
|
|
||||||
"prod:start": "NODE_ENV=production next start -p 3000"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/next-js": "^2.2.0",
|
"@faker-js/faker": "^9.5.1",
|
||||||
"@chakra-ui/react": "^2.8.2",
|
"dotenv": "^16.4.7",
|
||||||
"@emotion/react": "^11.11.4",
|
"express": "^4.21.2",
|
||||||
"@emotion/styled": "^11.11.0",
|
"faker": "^6.6.6"
|
||||||
"@twa-dev/sdk": "^7.0.0",
|
|
||||||
"@twa-dev/types": "^7.0.0",
|
|
||||||
"@types/cors": "^2.8.17",
|
|
||||||
"axios": "^1.6.7",
|
|
||||||
"bcryptjs": "^2.4.3",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"dotenv": "^16.3.1",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"framer-motion": "^11.0.8",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"mongoose": "^8.2.1",
|
|
||||||
"next": "13.5.8",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"zod": "^3.22.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"name": "campfireid",
|
||||||
"@types/express": "^4.17.18",
|
"version": "0.0.1",
|
||||||
"@types/node": "^20.11.24",
|
"description": "CampFire Identification Card (Telegram Mini APp)",
|
||||||
"@types/react": "^18.2.61",
|
"main": "index.js",
|
||||||
"@types/react-dom": "^18.2.19",
|
"devDependencies": {},
|
||||||
"eslint": "^8.57.0",
|
"scripts": {
|
||||||
"eslint-config-next": "13.5.8",
|
"test": "node live.js"
|
||||||
"prettier": "^3.0.3",
|
},
|
||||||
"typescript": "^5.3.3"
|
"repository": {
|
||||||
}
|
"type": "git",
|
||||||
|
"url": "https://git.campfiregg.ru/CampFire/CampFireID"
|
||||||
|
},
|
||||||
|
"author": "CampFire Solutions",
|
||||||
|
"license": "ISC"
|
||||||
}
|
}
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": [
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": false,
|
|
||||||
"noEmit": true,
|
|
||||||
"incremental": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"target": "ES2017"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"next-env.d.ts",
|
|
||||||
".next/types/**/*.ts",
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user