Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
768693f859 | |||
5f06f9b373 | |||
015d3eb5d1 | |||
27280c831a | |||
f7d4ae4741 | |||
e3aed97b22 | |||
3a19a84962 | |||
![]() |
8ad31ec05c | ||
6fbe286ca2 | |||
65a143ef4d | |||
33248392ff | |||
0e304a8b2e | |||
8af9c21cb1 | |||
8c152a47bd | |||
4a910fc576 | |||
aa7c6300a4 | |||
54516a66e9 | |||
c4328b7698 | |||
2af270e3d8 | |||
![]() |
2b86322b75 | ||
![]() |
afae5b4506 | ||
66ff498fa0 | |||
![]() |
5a252f6f29 | ||
![]() |
54011588e7 | ||
22cc9ef144 | |||
424d18f714 |
23
.env
Normal file
23
.env
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Настройки 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
Normal file
14
.env.example
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# 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
|
21
.env.production
Normal file
21
.env.production
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Настройки 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,2 +1,7 @@
|
|||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
/node_modules
|
/node_modules
|
||||||
|
/.next
|
||||||
|
/.next/static
|
||||||
|
/.next/cache
|
||||||
|
/.next/server
|
||||||
|
/.next
|
||||||
|
20
.next/app-build-manifest.json
Normal file
20
.next/app-build-manifest.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
19
.next/build-manifest.json
Normal file
19
.next/build-manifest.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"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
Normal file
BIN
.next/cache/webpack/client-development/0.pack.gz
vendored
Normal file
Binary file not shown.
BIN
.next/cache/webpack/client-development/1.pack.gz
vendored
Normal file
BIN
.next/cache/webpack/client-development/1.pack.gz
vendored
Normal file
Binary file not shown.
BIN
.next/cache/webpack/client-development/index.pack.gz
vendored
Normal file
BIN
.next/cache/webpack/client-development/index.pack.gz
vendored
Normal file
Binary file not shown.
BIN
.next/cache/webpack/client-development/index.pack.gz.old
vendored
Normal file
BIN
.next/cache/webpack/client-development/index.pack.gz.old
vendored
Normal file
Binary file not shown.
BIN
.next/cache/webpack/server-development/0.pack.gz
vendored
Normal file
BIN
.next/cache/webpack/server-development/0.pack.gz
vendored
Normal file
Binary file not shown.
BIN
.next/cache/webpack/server-development/index.pack.gz
vendored
Normal file
BIN
.next/cache/webpack/server-development/index.pack.gz
vendored
Normal file
Binary file not shown.
1
.next/package.json
Normal file
1
.next/package.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"type": "commonjs"}
|
1
.next/react-loadable-manifest.json
Normal file
1
.next/react-loadable-manifest.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
4
.next/server/app-paths-manifest.json
Normal file
4
.next/server/app-paths-manifest.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"/not-found": "app/not-found.js",
|
||||||
|
"/page": "app/page.js"
|
||||||
|
}
|
1
.next/server/middleware-build-manifest.js
Normal file
1
.next/server/middleware-build-manifest.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
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":[]}
|
6
.next/server/middleware-manifest.json
Normal file
6
.next/server/middleware-manifest.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"sortedMiddleware": [],
|
||||||
|
"middleware": {},
|
||||||
|
"functions": {},
|
||||||
|
"version": 2
|
||||||
|
}
|
1
.next/server/middleware-react-loadable-manifest.js
Normal file
1
.next/server/middleware-react-loadable-manifest.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
self.__REACT_LOADABLE_MANIFEST="{}"
|
1
.next/server/next-font-manifest.js
Normal file
1
.next/server/next-font-manifest.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"
|
1
.next/server/next-font-manifest.json
Normal file
1
.next/server/next-font-manifest.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}
|
1
.next/server/pages-manifest.json
Normal file
1
.next/server/pages-manifest.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
1
.next/server/server-reference-manifest.js
Normal file
1
.next/server/server-reference-manifest.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {}\n}"
|
4
.next/server/server-reference-manifest.json
Normal file
4
.next/server/server-reference-manifest.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"node": {},
|
||||||
|
"edge": {}
|
||||||
|
}
|
1
.next/static/chunks/polyfills.js
Normal file
1
.next/static/chunks/polyfills.js
Normal file
File diff suppressed because one or more lines are too long
1
.next/static/development/_buildManifest.js
Normal file
1
.next/static/development/_buildManifest.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
self.__BUILD_MANIFEST = {__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},sortedPages:["\u002F_app"]};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()
|
1
.next/static/development/_ssgManifest.js
Normal file
1
.next/static/development/_ssgManifest.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
|
1
.next/types/package.json
Normal file
1
.next/types/package.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"type": "module"}
|
49
app/api/user.ts
Normal file
49
app/api/user.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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' });
|
||||||
|
}
|
||||||
|
}
|
22
app/components/ClientOnly.tsx
Normal file
22
app/components/ClientOnly.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
'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}</>;
|
||||||
|
}
|
17
app/components/ClientPage.tsx
Normal file
17
app/components/ClientPage.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
'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 />;
|
||||||
|
}
|
47
app/components/MainApp.tsx
Normal file
47
app/components/MainApp.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
'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>
|
||||||
|
);
|
||||||
|
}
|
70
app/components/Shop.tsx
Normal file
70
app/components/Shop.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
'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>
|
||||||
|
);
|
||||||
|
}
|
87
app/components/TransferBalance.tsx
Normal file
87
app/components/TransferBalance.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
'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>
|
||||||
|
);
|
||||||
|
}
|
57
app/components/UserProfile.tsx
Normal file
57
app/components/UserProfile.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
'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>
|
||||||
|
);
|
||||||
|
}
|
79
app/hooks/useTelegramWebApp.ts
Normal file
79
app/hooks/useTelegramWebApp.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
'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 };
|
||||||
|
}
|
23
app/layout.tsx
Normal file
23
app/layout.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
67
app/models/User.ts
Normal file
67
app/models/User.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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);
|
5
app/page.tsx
Normal file
5
app/page.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import ClientPage from './components/ClientPage';
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return <ClientPage />;
|
||||||
|
}
|
123
app/pages/index.tsx
Normal file
123
app/pages/index.tsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
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;
|
25
app/providers.tsx
Normal file
25
app/providers.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
'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>
|
||||||
|
);
|
||||||
|
}
|
24
app/types/user.ts
Normal file
24
app/types/user.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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
Normal file
120
app/utils/api.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
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;
|
||||||
|
};
|
38
app/utils/db.ts
Normal file
38
app/utils/db.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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;
|
||||||
|
}
|
116
app/utils/demo.ts
Normal file
116
app/utils/demo.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
'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(),
|
||||||
|
});
|
67
app/utils/telegram.ts
Normal file
67
app/utils/telegram.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
30
backend/models/ShopItem.ts
Normal file
30
backend/models/ShopItem.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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);
|
81
backend/models/User.ts
Normal file
81
backend/models/User.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
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);
|
207
backend/server.ts
Normal file
207
backend/server.ts
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
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
Normal file
16
env.d.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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
Normal file
5
next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/// <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.
|
48
next.config.js
Normal file
48
next.config.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/** @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
57
package.json
57
package.json
@ -1,22 +1,45 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"name": "campfire-id",
|
||||||
"@faker-js/faker": "^9.5.1",
|
"version": "1.0.0",
|
||||||
"dotenv": "^16.4.7",
|
"private": true,
|
||||||
"express": "^4.21.2",
|
"description": "Telegram Mini App for user achievements and virtual economy",
|
||||||
"faker": "^6.6.6"
|
|
||||||
},
|
|
||||||
"name": "campfireid",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "CampFire Identification Card (Telegram Mini APp)",
|
|
||||||
"main": "index.js",
|
|
||||||
"devDependencies": {},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node live.js"
|
"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"
|
||||||
},
|
},
|
||||||
"repository": {
|
"dependencies": {
|
||||||
"type": "git",
|
"@chakra-ui/next-js": "^2.2.0",
|
||||||
"url": "https://git.campfiregg.ru/CampFire/CampFireID"
|
"@chakra-ui/react": "^2.8.2",
|
||||||
|
"@emotion/react": "^11.11.4",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@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"
|
||||||
},
|
},
|
||||||
"author": "CampFire Solutions",
|
"devDependencies": {
|
||||||
"license": "ISC"
|
"@types/express": "^4.17.18",
|
||||||
|
"@types/node": "^20.11.24",
|
||||||
|
"@types/react": "^18.2.61",
|
||||||
|
"@types/react-dom": "^18.2.19",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-next": "13.5.8",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
35
tsconfig.json
Normal file
35
tsconfig.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"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