Compare commits

..

6 Commits

Author SHA1 Message Date
c40bd412f9 feat: new README.md 2025-06-13 15:26:38 +03:00
dcc6bf448b fix: dockerfile prisma generate 2025-06-11 13:07:00 +03:00
3953e550b1 feat: multi-subs working 2025-06-11 11:54:28 +03:00
2738a8e8bd feat: started multi-subs 2025-06-10 17:01:44 +03:00
0a9bca073f feat: bot 2025-06-09 14:56:07 +03:00
fa112bc95a fix: env 2025-06-09 13:22:44 +03:00
24 changed files with 644 additions and 64 deletions

View File

@@ -12,4 +12,5 @@ helm-charts
.env
.editorconfig
.idea
coverage*
coverage*
bot

View File

@@ -15,13 +15,16 @@ COPY --from=install /temp/dev/node_modules node_modules
COPY . .
ENV NODE_ENV=production
RUN bunx prisma generate
RUN bun run build
FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /usr/src/app/.next .next
COPY --from=prerelease /usr/src/app/package.json .
COPY --from=prerelease /usr/src/app/prisma prisma
RUN bunx prisma generate
USER bun
EXPOSE 3000/tcp
ENTRYPOINT ["bun", "run", "start"]
ENTRYPOINT ["bun", "run", "start"]

View File

@@ -1,36 +1,86 @@
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).
# Telegram MiniApp для удобного информирования о прокси-подписке
## Getting Started
## Для инициализации
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
Сначала ставим пакеты:
```
bun i
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Потом ставим prisma приколы:
```
bunx prisma generate
```
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
Потом копируем .env.example в .env:
```
cp .env.example .env
```
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.
Заполняем его данными всякими (где про базу данных, нужно, чтобы URL соответствовал остальным параметрам), где XUI пишем авторизацию для 3X-UI.
## 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.
Для разработки поднимаем базу данных и редиску:
```
docker-compose up -d db redis
```
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
Заносим в базу данных таблицы:
```
bunx prisma db push
```
## Deploy on Vercel
Можно запускать приложение, оно будет на http://localhost:3000:
```
bun run dev
```
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.
Если нужно изменить schema.prisma, то нужно будет провести миграцию:
```
bunx prisma migrate dev --name *имя*
```
Для удобного взаимодействия с БД можно запустить prisma studio:
```
bunx prisma studio
```
### Для проверки в ТГ
В BotFather указываем у бота MiniApp url - `https://127.0.0.1:3000` и запускаем сервер:
```
bun run dev:https
```
---
## Для сборки и production запуска
```
docker compose up -d --build
```
### Если были изменения в БД:
```
docker compose exec app bunx prisma db push
```
---
## Подключение к БД сторонними клиентами
Подключиться к серверу:
```
ssh -L PORT1:localhost:PORT2 server
```
Где PORT1 - локальный порт, а PORT2 - порт БД на сервере. Подключаться нужно локально к PORT1.
То есть, можно указать в .env `DATABASE_URL=postgresql://user:password@localhost:PORT1/dbname` и открыть prisma studio:
```
bunx prisma studio
```

4
bot/.env.example Normal file
View File

@@ -0,0 +1,4 @@
NODE_ENV="development"
BOT_TOKEN=
BOT_MINIAPP_URL=

34
bot/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

27
bot/Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
FROM oven/bun:alpine AS base
WORKDIR /usr/src/app
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lock /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile
RUN mkdir -p /temp/prod
COPY package.json bun.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
ENV NODE_ENV=production
RUN bun run build
FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=prerelease /usr/src/app/package.json .
COPY --from=prerelease /usr/src/app/dist dist
USER bun
EXPOSE 3000/tcp
ENTRYPOINT ["bun", "run", "start:prod"]

203
bot/bun.lock Normal file
View File

@@ -0,0 +1,203 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "bot",
"dependencies": {
"dotenv": "^16.5.0",
"telegraf": "^4.16.3",
},
"devDependencies": {
"@types/bun": "latest",
"ts-patch": "^3.3.0",
"tsc-alias": "^1.8.16",
"tsconfig-paths": "^4.2.0",
"typescript-transform-paths": "^3.5.5",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@telegraf/types": ["@telegraf/types@7.1.0", "", {}, "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw=="],
"@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="],
"@types/node": ["@types/node@22.15.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"buffer-alloc": ["buffer-alloc@1.2.0", "", { "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" } }, "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow=="],
"buffer-alloc-unsafe": ["buffer-alloc-unsafe@1.1.0", "", {}, "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="],
"buffer-fill": ["buffer-fill@1.0.0", "", {}, "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="],
"bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
"dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"global-prefix": ["global-prefix@4.0.0", "", { "dependencies": { "ini": "^4.1.3", "kind-of": "^6.0.3", "which": "^4.0.0" } }, "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA=="],
"globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="],
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mylas": ["mylas@2.1.13", "", {}, "sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"p-timeout": ["p-timeout@4.1.0", "", {}, "sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"plimit-lit": ["plimit-lit@1.6.1", "", { "dependencies": { "queue-lit": "^1.5.1" } }, "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA=="],
"queue-lit": ["queue-lit@1.5.2", "", {}, "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"safe-compare": ["safe-compare@1.1.4", "", { "dependencies": { "buffer-alloc": "^1.2.0" } }, "sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ=="],
"sandwich-stream": ["sandwich-stream@2.0.2", "", {}, "sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"telegraf": ["telegraf@4.16.3", "", { "dependencies": { "@telegraf/types": "^7.1.0", "abort-controller": "^3.0.0", "debug": "^4.3.4", "mri": "^1.2.0", "node-fetch": "^2.7.0", "p-timeout": "^4.1.0", "safe-compare": "^1.1.4", "sandwich-stream": "^2.0.2" }, "bin": { "telegraf": "lib/cli.mjs" } }, "sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"ts-patch": ["ts-patch@3.3.0", "", { "dependencies": { "chalk": "^4.1.2", "global-prefix": "^4.0.0", "minimist": "^1.2.8", "resolve": "^1.22.2", "semver": "^7.6.3", "strip-ansi": "^6.0.1" }, "bin": { "ts-patch": "bin/ts-patch.js", "tspc": "bin/tspc.js" } }, "sha512-zAOzDnd5qsfEnjd9IGy1IRuvA7ygyyxxdxesbhMdutt8AHFjD8Vw8hU2rMF89HX1BKRWFYqKHrO8Q6lw0NeUZg=="],
"tsc-alias": ["tsc-alias@1.8.16", "", { "dependencies": { "chokidar": "^3.5.3", "commander": "^9.0.0", "get-tsconfig": "^4.10.0", "globby": "^11.0.4", "mylas": "^2.1.9", "normalize-path": "^3.0.0", "plimit-lit": "^1.2.6" }, "bin": { "tsc-alias": "dist/bin/index.js" } }, "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g=="],
"tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"typescript-transform-paths": ["typescript-transform-paths@3.5.5", "", { "dependencies": { "minimatch": "^9.0.5" }, "peerDependencies": { "typescript": ">=3.6.5" } }, "sha512-RMK86wKe/4+ad+3kMT9SKAs3K0tUHLe7hF+MLbD6VpC9VUmFuKorhf3pHz+qO5GdS4mUp2ncNUo14j6ws9UvBQ=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
}
}

25
bot/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "bot",
"private": true,
"main": "dist/main.js",
"scripts": {
"ts-prepare": "ts-patch install -s",
"start:dev": "bun run ts-prepare && bun ./src/main.ts",
"start:prod": "bun ./dist/main.js",
"build": "tsc && tsc-alias"
},
"devDependencies": {
"@types/bun": "latest",
"ts-patch": "^3.3.0",
"tsc-alias": "^1.8.16",
"tsconfig-paths": "^4.2.0",
"typescript-transform-paths": "^3.5.5"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"dotenv": "^16.5.0",
"telegraf": "^4.16.3"
}
}

42
bot/src/bot.ts Normal file
View File

@@ -0,0 +1,42 @@
import { Context, Telegraf as TelegramBot } from "telegraf";
import { Config } from "./config";
export class Bot {
private bot: TelegramBot;
constructor() {
this.bot = this.createTelegramBot();
}
public start() {
this.bot.start(this.handleBotStart);
this.bot.launch(this.handleBotLaunch);
}
private createTelegramBot() {
const botToken = Config.BOT_TOKEN;
return new TelegramBot(botToken);
}
private handleBotStart(ctx: Context) {
const webLink = Config.BOT_MINIAPP_URL;
ctx.reply("Привет! Чтобы начать, нажми на кнопку ниже.", {
reply_markup: {
inline_keyboard: [
[
{
text: "Открыть приложение",
web_app: {
url: webLink,
},
},
],
],
},
});
}
private handleBotLaunch() {
console.log("Bot started");
}
}

9
bot/src/config.ts Normal file
View File

@@ -0,0 +1,9 @@
import * as dotenv from "dotenv";
dotenv.config();
export const Config = {
NODE_ENV: process.env.NODE_ENV || "development",
BOT_TOKEN: process.env.BOT_TOKEN || "",
BOT_MINIAPP_URL: process.env.BOT_MINIAPP_URL || "",
};

8
bot/src/main.ts Normal file
View File

@@ -0,0 +1,8 @@
import { Bot } from "./bot";
function main() {
const bot = new Bot();
bot.start();
}
main();

30
bot/tsconfig.json Normal file
View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"baseUrl": "./",
"paths": {
"@/*": [
"./src/*"
]
},
"outDir": "dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"plugins": [
{
"transform": "typescript-transform-paths"
},
{
"transform": "typescript-transform-paths",
"afterDeclarations": true
}
]
},
"include": [
"src/**/*",
"src/config.ts"
]
}

View File

@@ -8,6 +8,8 @@
"@telegram-apps/init-data-node": "^2.0.7",
"@telegram-apps/sdk-react": "^3.3.0",
"@telegram-apps/telegram-ui": "^2.1.8",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"ioredis": "^5.6.1",
"lucide-react": "^0.513.0",
"next": "15.3.3",
@@ -492,6 +494,12 @@
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],
"embla-carousel-react": ["embla-carousel-react@8.6.0", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA=="],
"embla-carousel-reactive-utils": ["embla-carousel-reactive-utils@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="],
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],

View File

@@ -17,9 +17,20 @@ services:
volumes:
- db_data:/var/lib/postgresql/data
app:
env_file:
- ./.env
build: .
ports:
- "3900:3000"
depends_on:
- db
- redis
bot:
env_file:
- ./bot/.env
build: ./bot
depends_on:
- app
volumes:
db_data:
redis-data:

View File

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

View File

@@ -14,6 +14,8 @@
"@telegram-apps/init-data-node": "^2.0.7",
"@telegram-apps/sdk-react": "^3.3.0",
"@telegram-apps/telegram-ui": "^2.1.8",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"ioredis": "^5.6.1",
"lucide-react": "^0.513.0",
"next": "15.3.3",

View File

@@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "User_tgId_key";

View File

@@ -9,6 +9,6 @@ datasource db {
model User {
id String @id @default(uuid())
tgId String? @unique
tgId String?
email String? @unique
}

5
src/app/_dto/db.ts Normal file
View File

@@ -0,0 +1,5 @@
export interface User {
id: string;
tgId: string | null;
email: string | null;
}

View File

@@ -4,6 +4,7 @@ import { authFetch } from "@/lib/login";
import { getValidUrl } from "@/lib/url";
import { prisma } from "@/utils/prisma";
import { redis } from "@/utils/redis";
import { User } from "@prisma/client";
import { parse, validate } from "@telegram-apps/init-data-node";
import { ClientSettings, InboundResponse } from "./_dto/inbounds";
@@ -39,18 +40,19 @@ export async function getUrl(initData: string = "") {
throw new Error("User not found");
}
const cachedUser = await redis.get(`user:${initDataParsed.user.id}`);
const user = cachedUser
const users: User[] = cachedUser
? JSON.parse(cachedUser)
: await prisma.user.findFirst({
: await prisma.user.findMany({
where: {
tgId: initDataParsed.user.id.toString(),
},
});
if (!user) {
if (!users.length) {
throw new Error("User not found");
}
await redis.set(`user:${initDataParsed.user.id}`, JSON.stringify(user), "EX", 60);
return await getUrlApi(user.email || "");
await redis.set(`user:${initDataParsed.user.id}`, JSON.stringify(users), "EX", 60);
const usersUrl = await Promise.all(users.map(async (user) => await getUrlApi(user.email || "")));
return usersUrl;
} catch (e) {
throw e;
}

View File

@@ -1,35 +1,50 @@
"use client";
import { Block } from "@/components/Block";
import { Carousel } from "@/components/Carousel/Carousel";
import { Loader } from "@/components/Loader";
import { Page } from "@/components/Page";
import { cn } from "@/utils/cn";
import { useRawInitData } from "@telegram-apps/sdk-react";
import { ClipboardList } from "lucide-react";
import { ChevronLeft, ChevronRight, ClipboardList } from "lucide-react";
import { QRCodeSVG } from "qrcode.react";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { getUrl } from "./actions";
import { EmblaCarouselType } from "embla-carousel";
interface ProxySubData {
url: string;
expiryTime: number;
}
type ProxySubArray = ProxySubData[];
export default function Home() {
const [url, setUrl] = useState("");
const [expiryTime, setExpiryTime] = useState(0);
const [proxyData, setProxyData] = useState<ProxySubArray>([]);
const [chosen, setChosen] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [notFound, setNotFound] = useState(false);
const carouselApi = useRef<EmblaCarouselType | null>(null);
const initData = useRawInitData();
const onCopyClick = async () => {
if (!url.length) return;
const [isCopied, setIsCopied] = useState<boolean[]>([]);
useEffect(() => {
if (!proxyData.length) return;
setIsCopied(Array(proxyData.length).fill(false));
}, [proxyData]);
const onCopyClick = async (url: string) => {
if (!proxyData.length) return;
await navigator.clipboard.writeText(url);
setIsCopied(true);
setIsCopied((prev) => prev.map((_, i) => i === chosen));
setTimeout(() => {
setIsCopied(false);
setIsCopied((prev) => prev.map(() => false));
}, 2000);
};
const [isCopied, setIsCopied] = useState(false);
useEffect(() => {
const fetchData = async () => {
try {
const data = await getUrl(initData);
setUrl(data.url);
setExpiryTime(data.expiryTime);
setProxyData(data);
setIsLoading(false);
} catch (e) {
console.error(e);
@@ -57,40 +72,64 @@ export default function Home() {
<span className="text-xl font-semibold text-[var(--tg-theme-text-color)]">Nwaifu Proxy</span>
</header>
<main className="absolute bottom-0 left-0 flex w-full flex-col items-center gap-3 px-2.5 py-3.5 text-[var(--tg-theme-text-color)]">
{url && (
{proxyData.length && (
<>
<Block name="Ссылка">
<Block name={proxyData.length > 1 ? `Ссылка ${chosen + 1}/${proxyData.length}` : "Ссылка"}>
<div
className="relative flex size-52 flex-col items-center justify-center rounded-md bg-white"
onClick={onCopyClick}
className={cn(
"flex w-full flex-row items-center justify-between px-8",
proxyData.length === 1 && "justify-center"
)}
>
<div
className={cn(
"absolute top-0 left-0 flex h-full w-full flex-col items-center justify-center rounded-md bg-[var(--tg-theme-button-color)] opacity-0 transition-opacity",
isCopied && "opacity-95"
)}
>
<span className="flex flex-row text-lg font-semibold text-[var(--tg-theme-button-text-color)]">
Скопировано
<ClipboardList className="text-[var(--tg-theme-button-text-color)]" />
</span>
{proxyData.length > 1 && <ChevronLeft onClick={() => carouselApi.current?.scrollPrev()} />}
<div className="w-52">
<Carousel
loop
setApi={(api) => (carouselApi.current = api)}
onSelectIndex={(index) => setChosen(index)}
>
{proxyData.map((item, index) => (
<div
className="relative flex size-52 max-w-52 flex-[0_0_100%] flex-col items-center justify-center rounded-md bg-white"
onClick={() => onCopyClick(item.url)}
key={index}
>
<div
className={cn(
"absolute top-0 left-0 flex h-full w-full flex-col items-center justify-center rounded-md bg-[var(--tg-theme-button-color)] opacity-0 transition-opacity",
isCopied[index] && "opacity-95"
)}
>
<span className="flex flex-row text-lg font-semibold text-[var(--tg-theme-button-text-color)]">
Скопировано
<ClipboardList className="text-[var(--tg-theme-button-text-color)]" />
</span>
</div>
<QRCodeSVG value={item.url} className="size-48" />
</div>
))}
</Carousel>
</div>
<QRCodeSVG value={url} className="size-48" />
{proxyData.length > 1 && <ChevronRight onClick={() => carouselApi.current?.scrollNext()} />}
</div>
<span className="text-center text-sm font-semibold">Нажмите на QR, чтобы скопировать!</span>
</Block>
<Block name="Подписка">
<div className="flex flex-col items-center gap-0.5">
<span>Статус:</span>
<span>{expiryTime > Date.now() || expiryTime === 0 ? "Активна" : "Не Активна"}</span>
<span>
{proxyData[chosen].expiryTime > Date.now() || proxyData[chosen].expiryTime === 0
? "Активна"
: "Не Активна"}
</span>
</div>
<div className="flex flex-col items-center gap-0.5">
<span>Активна до:</span>
<span>
{expiryTime === 0
{proxyData[chosen].expiryTime === 0
? "Всегда"
: expiryTime > Date.now()
? new Date(expiryTime).toLocaleString("ru-RU")
: proxyData[chosen].expiryTime > Date.now()
? new Date(proxyData[chosen].expiryTime).toLocaleString("ru-RU")
: "Не Активна"}
</span>
</div>

View File

@@ -1,10 +1,16 @@
import { cn } from "@/utils/cn";
import { PropsWithChildren } from "react";
type BlockProps = PropsWithChildren<{ name?: string }>;
type BlockProps = PropsWithChildren<{ name?: string; className?: string }>;
const Block: React.FC<BlockProps> = ({ children, name = "" }) => {
const Block: React.FC<BlockProps> = ({ children, name = "", className = "" }) => {
return (
<div className="flex h-fit w-full flex-col items-center gap-2.5 rounded-lg bg-[var(--tg-theme-section-bg-color)] py-2.5 shadow-md">
<div
className={cn(
"flex h-fit w-full flex-col items-center gap-2.5 rounded-lg bg-[var(--tg-theme-section-bg-color)] py-2.5 shadow-md",
className
)}
>
{name && <span className="text-semibold text-lg">{name}</span>}
{children}
</div>

View File

@@ -0,0 +1,38 @@
"use client";
import { EmblaCarouselType, EmblaOptionsType } from "embla-carousel";
import useEmblaCarousel from "embla-carousel-react";
import { PropsWithChildren, useEffect, useState } from "react";
type Props = PropsWithChildren &
EmblaOptionsType & {
setApi?: (api: EmblaCarouselType) => void;
onSelectIndex?: (index: number) => void;
};
const Carousel: React.FC<Props> = ({ children, setApi, onSelectIndex, ...options }: Props) => {
const [emblaRef, emblaApi] = useEmblaCarousel(options);
const [, setSelectedIndex] = useState(0);
useEffect(() => {
if (!emblaApi) return;
if (emblaApi && setApi) setApi(emblaApi);
const selectHandler = () => {
const index = emblaApi.selectedScrollSnap();
setSelectedIndex(index);
onSelectIndex?.(index);
};
emblaApi.on("select", selectHandler);
return () => {
emblaApi.off("select", selectHandler);
};
}, [emblaApi, setApi, onSelectIndex]);
return (
<>
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex">{children}</div>
</div>
</>
);
};
export { Carousel };

View File

@@ -0,0 +1,30 @@
"use client";
import { cn } from "@/utils/cn";
type Props = {
itemsLength: number;
selectedIndex: number;
};
const Dots: React.FC<Props> = ({ itemsLength, selectedIndex }) => {
const arr = new Array(itemsLength).fill(0);
return (
<div className="my-2 flex -translate-y-5 justify-center gap-1">
{arr.map((_, i) => {
const selected = i === selectedIndex;
return (
<span
className={cn(
"h-2 w-2 rounded-full bg-[--tg-theme-button-bg-color] transition-all duration-300",
!selected && "opacity-50"
)}
key={i}
/>
);
})}
</div>
);
};
Dots.displayName = "Dots";
export { Dots };