Initial commit

This commit is contained in:
DosAi 2025-06-04 02:46:45 +03:00
commit 02de654266
20 changed files with 10184 additions and 0 deletions

119
.gitignore vendored Normal file
View File

@ -0,0 +1,119 @@
# ====================
# Global / Root
# ====================
# OS files
.DS_Store
Thumbs.db
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Environment files
.env
.env.*
!.env.example
# Lockfiles (if you choose to ignore lockfiles, otherwise you can remove these lines)
# /package-lock.json
# /yarn.lock
# /pnpm-lock.yaml
# VSCode workspace settings
.vscode/
# IDE files
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
# ====================
# Backend (Express + bot)
# ====================
/backend/node_modules/
/backend/.env
/backend/.env.*
/backend/logs/
/backend/*.log
# If you compile/transpile backend to a dist/ or build/ folder, ignore it:
# /backend/dist/
/backend/build/
# ====================
# Frontend (Next.js)
# ====================
/frontend/node_modules/
/frontend/.next/
/frontend/out/
/frontend/.vercel/
/frontend/.cache/
/frontend/.env
/frontend/.env.*
/frontend/.turbo/
/frontend/.turbo-cache/
/frontend/coverage/
# If using Swift/TypeScript build caching
/frontend/*.tsbuildinfo
# Ignore type declarations generated by Next
/frontend/next-env.d.ts
# ====================
# Miscellaneous
# ====================
# PEM certificates, private keys
*.pem
# Temporary or swap files
*.swp
*.swo
# npm / yarn / pnpm files
.pnp
.pnp.js
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# Temporary folders from Next.js exports
.env.local
.env.development.local
.env.test.local
.env.production.local
# Coverage reports
coverage/
.jest/
# Docker
docker-compose.override.yml
docker-compose.*.yml
# Generated log files
*.log
# macOS Trash
.Trashes
# Thumbnails and metadata files
*.db
*.seed
# Editor visual state files
*.sublime-workspace
*.sublime-project
# End of .gitignore

181
backend/bot.js Normal file
View File

@ -0,0 +1,181 @@
// backend/bot.js
// ───────────────────────────────────────────────────────────────────────────────
// 1) Устанавливаем зависимости:
// npm install express body-parser node-telegram-bot-api
//
// 2) Запуск:
// node bot.js
// (или: npx nodemon bot.js — для auto-reload при изменениях)
// ───────────────────────────────────────────────────────────────────────────────
const express = require('express');
const bodyParser = require('body-parser');
const TelegramBot = require('node-telegram-bot-api');
const crypto = require('crypto');
// Добавляем multer и streamifier
const multer = require('multer');
const streamifier = require('streamifier');
const app = express();
const HTTP_PORT = 3001; // порт для BACKEND API
const BOT_TOKEN = '7801636590:AAFphqOK0Dqta7v9VCLkTPGYC1OujNIFgXA'; // ← замените на свой токен
const WEBAPP_BASE_URL = 'https://d472-46-246-24-76.ngrok-free.app/'; // ← например: https://xyz123.ngrok.io
// 1) Запускаем бота (polling)
const bot = new TelegramBot(BOT_TOKEN, { polling: true });
// 2) Настраиваем multer (храним файлы в памяти)
const storage = multer.memoryStorage();
const upload = multer({ storage });
// 3) Разрешаем CORS для фронтенда
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type');
next();
});
// 4) JSON-парсер (не обязателен для multipart/form-data)
app.use(bodyParser.json());
// 5) Обработчик POST /api/submit (multipart/form-data)
app.post(
'/api/submit',
upload.single('photo'), // поле "photo" — это файл, если есть
(req, res) => {
try {
// Текстовые поля придут в req.body, файл — в req.file
const {
chat_id,
soapName,
weight,
basePrice,
aromaPrice,
aromaWeight,
pigmentPrice,
pigmentWeight,
moldPrice,
box,
filler,
ribbon,
labelValue,
markup,
totalCost,
finalPrice,
pricePer100g,
} = req.body;
// Проверяем обязательные поля
if (!chat_id) {
return res.status(400).send('chat_id не передан');
}
if (!soapName) {
return res.status(400).send('soapName не передан');
}
// Соберём сообщение так, чтобы в чат пришло всё, что ввели:
// 1. Название мыла
// 2. Вес и цена основы
// 3. Отдушка
// 4. Пигмент
// 5. Форма
// 6. Упаковка
// 7. Наценка
// 8. Итоги
let text = `🧼 <b>Расчёт мыла:</b> <i>${soapName}</i>\n\n`;
text += `🔹 <b>Вес мыла:</b> ${weight} г\n`;
text += `🔹 <b>Цена за 1 кг основы:</b> ${basePrice} ₽/кг\n\n`;
text += `🔹 <b>Отдушка:</b> ${aromaWeight} г по ${aromaPrice} ₽/фасовка\n`;
text += `🔹 <b>Пигмент:</b> ${pigmentWeight} г по ${pigmentPrice} ₽/фасовка\n\n`;
text += `🔹 <b>Цена формы:</b> ${moldPrice}\n\n`;
text += `🔹 <b>Упаковка:</b>\n`;
text += ` • Пакет/коробка: ${box}\n`;
text += ` • Наполнитель: ${filler}\n`;
text += ` • Лента: ${ribbon}\n`;
text += ` • Наклейка: ${labelValue}\n\n`;
text += `🔹 <b>Наценка:</b> ${markup}%\n\n`;
text += `📊 <b>Итоги расчёта:</b>\n`;
text += ` • Себестоимость: ${Number(totalCost).toFixed(1)}\n`;
text += ` • Итоговая цена с наценкой: ${Number(finalPrice).toFixed(1)}\n`;
text += ` • Цена за 100 г: ${Number(pricePer100g).toFixed(1)}`;
// Если пользователь прикрепил фото (req.file), шлём sendPhoto
if (req.file) {
const bufferStream = streamifier.createReadStream(req.file.buffer);
return bot
.sendPhoto(
Number(chat_id),
bufferStream,
{ caption: text, parse_mode: 'HTML' }
)
.then(() => res.sendStatus(200))
.catch((err) => {
console.error('Ошибка при отправке фото:', err);
res.status(500).send('Ошибка при отправке фото ботом');
});
}
// Если фото не было, просто шлём текст
bot
.sendMessage(Number(chat_id), text, { parse_mode: 'HTML' })
.then(() => res.sendStatus(200))
.catch((err) => {
console.error('Ошибка при отправке сообщения:', err);
res.sendStatus(500);
});
} catch (err) {
console.error('Ошибка в /api/submit:', err);
res.status(500).send('Внутренняя ошибка сервера');
}
}
);
// 6) Команда /menu — отправляем inline-кнопку с chat_id
bot.setMyCommands([
{ command: 'menu', description: 'Открыть калькулятор' },
]);
bot.onText(/\/menu/, (msg) => {
try {
const chatId = msg.chat.id;
const url = `${WEBAPP_BASE_URL}/?chat_id=${chatId}`;
bot.sendMessage(
chatId,
'Нажмите кнопку ниже, чтобы открыть калькулятор:',
{
reply_markup: {
inline_keyboard: [
[
{
text: 'Открыть калькулятор',
web_app: { url },
},
],
],
},
}
);
} catch (err) {
console.error('Ошибка в обработчике /menu:', err);
}
});
// 7) Ловим ошибки polling-а и логируем детали
bot.on('polling_error', (err) => {
console.error('Polling error:', err);
});
// 8) Запускаем Express-сервер на порту 3001
app.listen(HTTP_PORT, () => {
console.log(`Bot+API запущены, слушаем порт ${HTTP_PORT}`);
});

5
backend/nodemon.json Normal file
View File

@ -0,0 +1,5 @@
{
"watch": ["."],
"ext": "js,json",
"exec": "node bot.js"
}

3135
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
backend/package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"body-parser": "^2.2.0",
"express": "^5.1.0",
"multer": "^2.0.0",
"node-telegram-bot-api": "^0.66.0",
"streamifier": "^0.1.1"
}
}

36
frontend/README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

BIN
frontend/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

12
frontend/app/globals.css Normal file
View File

@ -0,0 +1,12 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

34
frontend/app/layout.tsx Normal file
View File

@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

7
frontend/app/page.tsx Normal file
View File

@ -0,0 +1,7 @@
import SoapCalculator from "@/components/SoapCalculator";
export default function Home() {
return (
<SoapCalculator />
);
}

View File

@ -0,0 +1,336 @@
// components/SoapCalculator.tsx
'use client';
import { useState, useEffect, ChangeEvent } from 'react';
import { calculateTotal } from '@/lib/calc';
type InputNumberProps = {
label: string;
value: string;
onChange: (v: string) => void;
placeholder?: string;
};
const InputNumber = ({ label, value, onChange, placeholder }: InputNumberProps) => (
<label className="flex flex-col gap-1">
<span>{label}</span>
<input
type="number"
className="border px-2 py-1 rounded text-black"
value={value}
placeholder={placeholder}
onChange={(e) => onChange(e.target.value)}
/>
</label>
);
const CostBlock = ({
title,
value,
highlight = false,
}: {
title: string;
value: number;
highlight?: boolean;
}) => (
<div
className={`p-2 ${highlight ? 'bg-red-200 font-semibold' : 'bg-lime-100'}`}
>
{title}: {value.toFixed(1)} руб
</div>
);
export default function SoapCalculator() {
// 1) Поля ввода как строки (по умолчанию пустые)
const [soapName, setSoapName] = useState('');
const [weight, setWeight] = useState(''); // г
const [basePrice, setBasePrice] = useState(''); // руб/кг
const [aromaPrice, setAromaPrice] = useState(''); // руб/фасовка
const [aromaWeight, setAromaWeight] = useState(''); // г фасовка
const [pigmentPrice, setPigmentPrice] = useState(''); // руб/фасовка
const [pigmentWeight, setPigmentWeight] = useState(''); // г фасовка
const [moldPrice, setMoldPrice] = useState(''); // руб за форму
const [box, setBox] = useState(''); // руб
const [filler, setFiller] = useState(''); // руб
const [ribbon, setRibbon] = useState(''); // руб
const [labelValue, setLabelValue] = useState(''); // руб наклейка
const [markup, setMarkup] = useState(''); // %
// Файл фотографии и chat_id
const [photoFile, setPhotoFile] = useState<File | null>(null);
const [chatId, setChatId] = useState<string | null>(null);
// 2) Извлекаем chat_id из URL при монтировании
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const id = params.get('chat_id');
if (id) {
setChatId(id);
}
}, []);
// 3) Преобразуем строковые поля в числа (или 0, если не число)
const toNum = (str: string) => {
const n = parseFloat(str.replace(',', '.'));
return isNaN(n) ? 0 : n;
};
const weightNum = toNum(weight);
const basePriceNum = toNum(basePrice);
const aromaPriceNum = toNum(aromaPrice);
const aromaWeightNum = toNum(aromaWeight);
const pigmentPriceNum = toNum(pigmentPrice);
const pigmentWeightNum = toNum(pigmentWeight);
const moldPriceNum = toNum(moldPrice);
const boxNum = toNum(box);
const fillerNum = toNum(filler);
const ribbonNum = toNum(ribbon);
const labelNum = toNum(labelValue);
const markupNum = toNum(markup);
// 4) Считаем все базовые значения через calculateTotal
const result = calculateTotal({
weight: weightNum,
basePrice: basePriceNum,
aromaPrice: aromaPriceNum,
aromaWeight: aromaWeightNum,
pigmentPrice: pigmentPriceNum,
pigmentWeight: pigmentWeightNum,
moldPrice: moldPriceNum,
packaging: {
box: boxNum,
filler: fillerNum,
ribbon: ribbonNum,
label: labelNum,
},
});
// 5) Итоговые значения
const finalPrice = result.total * (1 + markupNum / 100);
const pricePer100g = weightNum > 0 ? (finalPrice / weightNum) * 100 : 0;
// 6) Обработчик изменения фото
const handlePhotoChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setPhotoFile(e.target.files[0]);
} else {
setPhotoFile(null);
}
};
// 7) Обработчик отправки формы на backend
const handleSubmit = async () => {
if (!chatId) {
alert('❗ Не найден chat_id. Откройте калькулятор через Telegram-бота.');
return;
}
// Собираем FormData
const formData = new FormData();
formData.append('chat_id', chatId);
formData.append('soapName', soapName || '');
formData.append('weight', weightNum.toString());
formData.append('basePrice', basePriceNum.toString());
formData.append('aromaPrice', aromaPriceNum.toString());
formData.append('aromaWeight', aromaWeightNum.toString());
formData.append('pigmentPrice', pigmentPriceNum.toString());
formData.append('pigmentWeight', pigmentWeightNum.toString());
formData.append('moldPrice', moldPriceNum.toString());
formData.append('box', boxNum.toString());
formData.append('filler', fillerNum.toString());
formData.append('ribbon', ribbonNum.toString());
formData.append('labelValue', labelNum.toString());
formData.append('markup', markupNum.toString());
formData.append('totalCost', result.total.toString());
formData.append('finalPrice', finalPrice.toString());
formData.append('pricePer100g', pricePer100g.toString());
if (photoFile) {
formData.append('photo', photoFile);
}
try {
// Замените URL на публичный адрес вашего backend (Express/ngrok/другой хостинг)
// Например: 'https://abcd1234.ngrok.io/api/submit'
const res = await fetch('http://91.122.47.159:3001/api/submit', {
method: 'POST',
body: formData,
});
if (res.ok) {
alert('✅ Расчёт успешно отправлен в Telegram!');
// Сбрасываем все поля на пустые строки / null
setSoapName('');
setWeight('');
setBasePrice('');
setAromaPrice('');
setAromaWeight('');
setPigmentPrice('');
setPigmentWeight('');
setMoldPrice('');
setBox('');
setFiller('');
setRibbon('');
setLabelValue('');
setMarkup('');
setPhotoFile(null);
// Прокручиваем страницу вверх
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
const text = await res.text();
alert(`Ошибка при отправке: ${text}`);
}
} catch (err) {
console.error(err);
alert('Ошибка сети при отправке расчёта');
}
};
return (
<div className="max-w-xl mx-auto p-4 space-y-6 text-sm">
{chatId === null && (
<div className="bg-yellow-100 p-2 text-yellow-800 font-semibold">
Не найден chat_id. Откройте калькулятор через Telegram-бота.
</div>
)}
{/* Название мыла */}
<label className="flex flex-col gap-1">
<span>Название мыла</span>
<input
type="text"
className="border px-2 py-1 rounded text-black"
value={soapName}
onChange={(e) => setSoapName(e.target.value)}
placeholder="Например: Лавандовое"
/>
</label>
{/* Фото мыла */}
<label className="flex flex-col gap-1">
<span>Фото мыла (необязательно)</span>
<input type="file" accept="image/*" onChange={handlePhotoChange} />
</label>
{photoFile && (
<img
src={URL.createObjectURL(photoFile)}
alt="Предпросмотр мыла"
className="w-32 h-32 object-cover rounded"
/>
)}
{/* Блок «Основа» */}
<div className="grid grid-cols-2 gap-4">
<InputNumber
label="Вес мыла, г"
value={weight}
onChange={setWeight}
placeholder="Введите, например 60"
/>
<InputNumber
label="Цена за 1 кг основы, руб"
value={basePrice}
onChange={setBasePrice}
placeholder="Введите, например 1000"
/>
</div>
<CostBlock title="Себестоимость основы" value={result.base} />
{/* Блок «Отдушка» */}
<div className="grid grid-cols-2 gap-4">
<InputNumber
label="Цена отдушки, руб"
value={aromaPrice}
onChange={setAromaPrice}
placeholder="Введите, например 300"
/>
<InputNumber
label="Фасовка отдушки, г"
value={aromaWeight}
onChange={setAromaWeight}
placeholder="Введите, например 50"
/>
</div>
<CostBlock title="Себестоимость отдушки (1 %)" value={result.aroma} />
{/* Блок «Пигмент» */}
<div className="grid grid-cols-2 gap-4">
<InputNumber
label="Цена пигмента, руб"
value={pigmentPrice}
onChange={setPigmentPrice}
placeholder="Введите, например 20"
/>
<InputNumber
label="Фасовка пигмента, г"
value={pigmentWeight}
onChange={setPigmentWeight}
placeholder="Введите, например 10"
/>
</div>
<CostBlock title="Себестоимость пигмента (0.5 %)" value={result.pigment} />
{/* Блок «Форма» */}
<InputNumber
label="Цена формы, руб"
value={moldPrice}
onChange={setMoldPrice}
placeholder="Введите, например 500"
/>
<CostBlock title="Себестоимость формы" value={result.mold} />
{/* Блок «Упаковка» */}
<div className="grid grid-cols-2 gap-4">
<InputNumber
label="Пакет/коробка, руб"
value={box}
onChange={setBox}
placeholder="Введите, например 20"
/>
<InputNumber
label="Наполнитель, руб"
value={filler}
onChange={setFiller}
placeholder="Введите, например 5"
/>
<InputNumber
label="Лента, руб"
value={ribbon}
onChange={setRibbon}
placeholder="Введите, например 1"
/>
<InputNumber
label="Наклейка, руб"
value={labelValue}
onChange={setLabelValue}
placeholder="Введите, например 1"
/>
</div>
<CostBlock title="Стоимость упаковки" value={result.packaging} />
<CostBlock title="Операционные расходы (5 %)" value={result.operational} />
<CostBlock title="Итого себестоимость" value={result.total} highlight />
{/* Блок «Наценка и цена 100 г» */}
<div className="grid grid-cols-2 gap-4">
<InputNumber
label="Наценка, %"
value={markup}
onChange={setMarkup}
placeholder="Введите, например 30"
/>
</div>
<CostBlock title="Итоговая цена с наценкой" value={finalPrice} />
<CostBlock title="Цена за 100 г" value={pricePer100g} />
{/* Кнопка «Отправить расчёт» */}
<button
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded"
onClick={handleSubmit}
>
Отправить расчёт в Telegram
</button>
</div>
);
}

View File

@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

53
frontend/lib/calc.ts Normal file
View File

@ -0,0 +1,53 @@
// lib/calc.ts
type Input = {
weight: number;
basePrice: number;
aromaPrice: number;
aromaWeight: number;
pigmentPrice: number;
pigmentWeight: number;
moldPrice: number;
packaging: {
box: number;
filler: number;
ribbon: number;
label: number;
};
};
export function calculateTotal(data: Input) {
const {
weight,
basePrice,
aromaPrice,
aromaWeight,
pigmentPrice,
pigmentWeight,
moldPrice,
packaging,
} = data;
const base = (weight / 1000) * basePrice;
const aroma = ((weight * 0.01) / aromaWeight) * aromaPrice;
const pigment = ((weight * 0.005) / pigmentWeight) * pigmentPrice;
const mold = moldPrice / 100;
const packagingCost = packaging.box + packaging.filler + packaging.ribbon + packaging.label;
const subtotal = base + aroma + pigment + mold + packagingCost;
const operational = subtotal * 0.05;
const total = subtotal + operational;
return {
base: round(base),
aroma: round(aroma),
pigment: round(pigment),
mold: round(mold),
packaging: round(packagingCost),
operational: round(operational),
total: round(total),
};
}
function round(val: number) {
return Math.round(val * 10) / 10;
}

5
frontend/next-env.d.ts vendored Normal file
View 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.

7
frontend/next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

6133
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
frontend/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.3.3"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "15.3.3",
"@eslint/eslintrc": "^3"
}
}

View File

@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

27
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

26
frontend/types/telegram.d.ts vendored Normal file
View File

@ -0,0 +1,26 @@
interface TelegramWebAppUser {
id: number;
is_bot: boolean;
first_name: string;
last_name?: string;
username?: string;
language_code?: string;
}
interface TelegramWebAppInitData {
user?: TelegramWebAppUser;
}
interface TelegramWebApp {
initData: string;
initDataUnsafe: TelegramWebAppInitData;
ready: () => void;
}
interface TelegramNamespace {
WebApp: TelegramWebApp;
}
interface Window {
Telegram?: TelegramNamespace;
}