Initial commit: Tapalka monorepo (bot, front, strapi)
This commit is contained in:
29
.claude/settings.local.json
Normal file
29
.claude/settings.local.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(dir:*)",
|
||||||
|
"Bash(npm run dev:*)",
|
||||||
|
"Bash(node --version:*)",
|
||||||
|
"Bash(nvm list:*)",
|
||||||
|
"Bash(npm rebuild:*)",
|
||||||
|
"Bash(npm install:*)",
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(python:*)",
|
||||||
|
"Bash(taskkill:*)",
|
||||||
|
"Bash(npx better-sqlite3-cli data/database.db \"UPDATE bot_configs SET token = ''8313420690:AAGnqTrnF-GuQnW1AwipA-XORo6BjLFWP0U''\")",
|
||||||
|
"Bash(where:*)",
|
||||||
|
"Bash(ngrok http:*)",
|
||||||
|
"Bash(netstat:*)",
|
||||||
|
"Bash(findstr:*)",
|
||||||
|
"Bash(winget install:*)",
|
||||||
|
"Bash(\"C:\\\\Program Files \\(x86\\)\\\\cloudflared\\\\cloudflared.exe\" tunnel --url http://localhost:3000)",
|
||||||
|
"Bash(timeout:*)",
|
||||||
|
"Bash(ping:*)",
|
||||||
|
"Bash(npm run develop:*)",
|
||||||
|
"Bash(npm run:*)",
|
||||||
|
"Bash(cloudflared tunnel:*)",
|
||||||
|
"Bash(JWT=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzcxMTU2NDgzLCJleHAiOjE3NzM3NDg0ODN9.i19Tho2VbzH7oKRmZOD8XnlVTinTjQMoH4XxA7ye0Ls\")",
|
||||||
|
"Bash(ADMIN_JWT=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzcxMTU2NDk5LCJleHAiOjE3NzM3NDg0OTl9.w4vKy5LCTnt6voK1ruamxvXwDjMW3zLzs7JlPHseh-Y\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Duplicate nested directory at repo root
|
||||||
|
/СlickerDomenik/
|
||||||
|
|
||||||
|
# Stray Windows reserved-name files (anywhere in repo)
|
||||||
|
nul
|
||||||
|
_nul
|
||||||
|
|
||||||
|
# Dependencies / build artifacts (catch-all, anywhere in repo)
|
||||||
|
node_modules/
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.strapi/
|
||||||
|
.strapi-updater.json
|
||||||
|
.strapi-cloud.json
|
||||||
|
coverage/
|
||||||
|
.tmp/
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Editor / OS
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Local-only env overrides (real .env files are committed per user request)
|
||||||
|
.env*.local
|
||||||
15
CMyTapper/robucks-bot/.env
Normal file
15
CMyTapper/robucks-bot/.env
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
STRAPI_USER="admin"
|
||||||
|
STRAPI_PASSWORD="Admin1234"
|
||||||
|
CRYPTO_SECRET=abcDef$
|
||||||
|
PORT=3001
|
||||||
|
|
||||||
|
# PROD
|
||||||
|
#WEBAPP_URL="https://rbx-click.ru"
|
||||||
|
#STRAPI_URL="https://strapi.rbx-click.ru/api/"
|
||||||
|
#STRAPI_URL_BASE=https://strapi.rbx-click.ru
|
||||||
|
|
||||||
|
# LOCAL
|
||||||
|
WEBAPP_URL=https://runtime-monster-journal-logic.trycloudflare.com
|
||||||
|
STRAPI_URL="http://127.0.0.1:1337/api/"
|
||||||
|
STRAPI_URL_BASE=http://127.0.0.1:1337
|
||||||
|
|
||||||
24
CMyTapper/robucks-bot/.eslintrc
Normal file
24
CMyTapper/robucks-bot/.eslintrc
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": ["@typescript-eslint", "prettier"],
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": ["warn", {
|
||||||
|
"endOfLine": "auto",
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"useTabs": false,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": "warn",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"prefer-const": "warn",
|
||||||
|
"no-console": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
CMyTapper/robucks-bot/.github/workflows/prod.yaml
vendored
Normal file
27
CMyTapper/robucks-bot/.github/workflows/prod.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Deploy to prod
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy with pm2
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{secrets.PROD_SSH_HOST}}
|
||||||
|
username: ${{secrets.PROD_SSH_USERNAME}}
|
||||||
|
password: ${{secrets.PROD_SSH_PASSWORD}}
|
||||||
|
script_stop: true
|
||||||
|
script: |
|
||||||
|
export NVM_DIR=~/.nvm
|
||||||
|
source ~/.nvm/nvm.sh
|
||||||
|
cd ${{secrets.PROD_PROJECT_PATH}}
|
||||||
|
git checkout main
|
||||||
|
git pull
|
||||||
|
yarn install
|
||||||
|
pm2 restart ecosystem.config.js
|
||||||
3
CMyTapper/robucks-bot/.gitignore
vendored
Normal file
3
CMyTapper/robucks-bot/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.idea
|
||||||
|
/node_modules
|
||||||
|
/build
|
||||||
10
CMyTapper/robucks-bot/ecosystem.config.js
Normal file
10
CMyTapper/robucks-bot/ecosystem.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'bot',
|
||||||
|
script: 'npm run start',
|
||||||
|
time: true,
|
||||||
|
log_date_format: 'DD.MM.YYYY HH:mm:ss',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
6
CMyTapper/robucks-bot/nodemon.json
Normal file
6
CMyTapper/robucks-bot/nodemon.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"watch": ["src"],
|
||||||
|
"ext": ".ts,.js",
|
||||||
|
"ignore": [],
|
||||||
|
"exec": "npx ts-node ./src/index.ts"
|
||||||
|
}
|
||||||
3903
CMyTapper/robucks-bot/package-lock.json
generated
Normal file
3903
CMyTapper/robucks-bot/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
CMyTapper/robucks-bot/package.json
Normal file
40
CMyTapper/robucks-bot/package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "clicker-bot",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"dev": "npx nodemon",
|
||||||
|
"start": "npx ts-node src/index.ts"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/axios": "^0.14.0",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
|
"@typescript-eslint/parser": "^7.14.1",
|
||||||
|
"axios": "^1.7.2",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"eslint": "8",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"telegraf": "^4.16.3",
|
||||||
|
"typescript": "^5.5.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/node": "^20.14.9",
|
||||||
|
"nodemon": "^3.1.4",
|
||||||
|
"rimraf": "^5.0.7",
|
||||||
|
"ts-node": "^10.9.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
63
CMyTapper/robucks-bot/src/api/players.api.ts
Normal file
63
CMyTapper/robucks-bot/src/api/players.api.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { AxiosInstance } from 'axios';
|
||||||
|
import { HttpInstanceFactory } from '../utils/HttpsInstanceFactory';
|
||||||
|
|
||||||
|
export class PlayersApi {
|
||||||
|
private static instance: PlayersApi;
|
||||||
|
private httpInstance: AxiosInstance;
|
||||||
|
private populateQuery = 'populate=referral&populate=my_referrals';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.httpInstance = HttpInstanceFactory.getAuthorizedInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance() {
|
||||||
|
if (!this.instance) {
|
||||||
|
this.instance = new PlayersApi();
|
||||||
|
}
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async connectReferrals(referralId: number, invitedId: number) {
|
||||||
|
await this.updatePlayer(referralId, {
|
||||||
|
data: {
|
||||||
|
my_referrals: {
|
||||||
|
connect: [invitedId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await this.updatePlayer(invitedId, {
|
||||||
|
data: {
|
||||||
|
referral: {
|
||||||
|
set: [referralId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updatePlayer(id: number | string, data: any) {
|
||||||
|
return (await this.httpInstance.put(`players/${id}?${this.populateQuery}`, data)).data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createPlayer(data: any) {
|
||||||
|
const createdPlayer = (await this.httpInstance.post(`players?${this.populateQuery}`, data))
|
||||||
|
.data.data;
|
||||||
|
await this.httpInstance.post(`/archive-requests`, {
|
||||||
|
data: {
|
||||||
|
player: {
|
||||||
|
connect: [createdPlayer.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return createdPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPlayerByTelegramId(id: number | string) {
|
||||||
|
const filteredPlayers = (
|
||||||
|
await this.httpInstance.get(`players?filters[telegram_id]=${id}&${this.populateQuery}`)
|
||||||
|
).data.data as any[];
|
||||||
|
if (filteredPlayers.length > 0) {
|
||||||
|
return filteredPlayers[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
CMyTapper/robucks-bot/src/bot.ts
Normal file
106
CMyTapper/robucks-bot/src/bot.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { Markup, Telegraf } from 'telegraf';
|
||||||
|
import { PlayersApi } from './api/players.api';
|
||||||
|
import { encryptId } from './utils/encrypter';
|
||||||
|
import { NotifyParams } from './index';
|
||||||
|
|
||||||
|
class Bot {
|
||||||
|
private botInstance: Telegraf;
|
||||||
|
|
||||||
|
constructor(token: string) {
|
||||||
|
this.botInstance = new Telegraf(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async launch() {
|
||||||
|
process.once('SIGINT', () => this.botInstance.stop('SIGINT'));
|
||||||
|
process.once('SIGTERM', () => this.botInstance.stop('SIGTERM'));
|
||||||
|
|
||||||
|
await this.botInstance.launch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public start() {
|
||||||
|
this.botInstance.start(async ctx => {
|
||||||
|
const playersApi = PlayersApi.getInstance();
|
||||||
|
try {
|
||||||
|
const user_id = String(ctx.from.id);
|
||||||
|
const username = ctx.from.username || ctx.from.first_name;
|
||||||
|
|
||||||
|
const encrypted_id = encryptId(user_id);
|
||||||
|
let me = await playersApi.getPlayerByTelegramId(user_id);
|
||||||
|
|
||||||
|
if (!me) {
|
||||||
|
me = await playersApi.createPlayer({
|
||||||
|
data: {
|
||||||
|
telegram_id: user_id,
|
||||||
|
telegram_nick: username,
|
||||||
|
balance: 0,
|
||||||
|
old_balance: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await playersApi.updatePlayer(me.id, {
|
||||||
|
data: {
|
||||||
|
auth_id: encrypted_id,
|
||||||
|
telegram_nick: username,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.reply(
|
||||||
|
'Привет! Нажми на кнопку ниже, чтобы открыть веб-приложение.',
|
||||||
|
Markup.inlineKeyboard([
|
||||||
|
[
|
||||||
|
Markup.button.webApp(
|
||||||
|
'Открыть WebApp',
|
||||||
|
`${process.env.WEBAPP_URL}/${encodeURIComponent(encrypted_id)}/home` ||
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const args = ctx.message.text.split(' ');
|
||||||
|
let referral_user_id = null;
|
||||||
|
if (args.length > 1) {
|
||||||
|
referral_user_id = +args[1].substring(4);
|
||||||
|
}
|
||||||
|
const sender = await playersApi.getPlayerByTelegramId(referral_user_id || -1)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!sender ||
|
||||||
|
sender.id === me.id ||
|
||||||
|
!!me.attributes.referral.data ||
|
||||||
|
me.attributes.my_referrals.data.some(
|
||||||
|
(referral: { id: number }) => referral.id === sender.id,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await playersApi.connectReferrals(sender.id, me.id);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('ERROR: /start', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public notifyByNewExchange({ exchange_id, telegram_id }: NotifyParams) {
|
||||||
|
const requestLink = `${process.env.STRAPI_URL_BASE}/admin/content-manager/collection-types/api::exchange-request.exchange-request/${exchange_id}`;
|
||||||
|
const msgTxt = `Поступила новая <a href="${requestLink}">заявка</a> на обмен!`;
|
||||||
|
for (const id of telegram_id) {
|
||||||
|
this.botInstance.telegram
|
||||||
|
.sendMessage(id, msgTxt, {
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
})
|
||||||
|
.catch(err => console.log('ERROR: notify on new exchange', err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBotLink() {
|
||||||
|
return `https://t.me/${this.botInstance.botInfo?.username}?start=`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
this.botInstance.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Bot;
|
||||||
122
CMyTapper/robucks-bot/src/index.ts
Normal file
122
CMyTapper/robucks-bot/src/index.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import express, { Response } from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import path from 'path';
|
||||||
|
import Bot from './bot';
|
||||||
|
import { HttpInstanceFactory } from './utils/HttpsInstanceFactory';
|
||||||
|
import { TypedRequestBody } from './types/express.type';
|
||||||
|
import { getDifferenceInDays } from './utils/GetDifferenceInDays';
|
||||||
|
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||||
|
|
||||||
|
export type NotifyParams = {
|
||||||
|
telegram_id: string[] | number[];
|
||||||
|
exchange_id: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export let bot: Bot;
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
const PORT = process.env.PORT as string;
|
||||||
|
const restartTimeout = 10;
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cors());
|
||||||
|
|
||||||
|
app.post('/notify', async (req: TypedRequestBody<NotifyParams>, res: Response) => {
|
||||||
|
bot.notifyByNewExchange(req.body);
|
||||||
|
|
||||||
|
return res.status(200).send({});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/link-bot', async (req, res) => {
|
||||||
|
const linkBot = bot.getBotLink();
|
||||||
|
|
||||||
|
return res.status(200).send({ link: linkBot });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/config-bot-data', async (req, res) => {
|
||||||
|
HttpInstanceFactory.getAuthorizedInstance()
|
||||||
|
.get('/config')
|
||||||
|
.then(async response => {
|
||||||
|
const { token, dateLifetimeJwt } = response.data.data.attributes;
|
||||||
|
|
||||||
|
if (!token || getDifferenceInDays(new Date(), dateLifetimeJwt) >= 30) {
|
||||||
|
const baseInstance = HttpInstanceFactory.getBaseInstance();
|
||||||
|
const dateLifeTime = new Date();
|
||||||
|
|
||||||
|
const user = await baseInstance.post(`auth/local`, {
|
||||||
|
identifier: process.env.STRAPI_USER ?? '',
|
||||||
|
password: process.env.STRAPI_PASSWORD ?? '',
|
||||||
|
});
|
||||||
|
|
||||||
|
await HttpInstanceFactory.getAuthorizedInstance().put(`config`, {
|
||||||
|
data: {
|
||||||
|
token: user.data.jwt,
|
||||||
|
dateLifetimeJwt: dateLifeTime,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
token: user.data.jwt,
|
||||||
|
dateLifetimeJwt: dateLifeTime,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return res.status(200).send({
|
||||||
|
token: token,
|
||||||
|
dateLifetimeJwt: dateLifetimeJwt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/restart-bot', async (req, res) => {
|
||||||
|
if (bot) {
|
||||||
|
try {
|
||||||
|
await bot.stop();
|
||||||
|
console.log('Bot stopped successfully.');
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error stopping the bot:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
launchBot();
|
||||||
|
|
||||||
|
return res.status(200).send({ message: 'Bot is restarting...' });
|
||||||
|
});
|
||||||
|
|
||||||
|
const launchBot = async () => {
|
||||||
|
console.log(`Starting on port ${PORT}...`);
|
||||||
|
|
||||||
|
HttpInstanceFactory.updateAuthorizedInstance()
|
||||||
|
.then(() => {
|
||||||
|
HttpInstanceFactory.getAuthorizedInstance()
|
||||||
|
.get('bot-config')
|
||||||
|
.then(res => {
|
||||||
|
bot = new Bot(res.data.data.attributes.token);
|
||||||
|
bot.launch().catch(() => {
|
||||||
|
console.log(`ERROR: launching bot. Restarting in ${restartTimeout} sec.`);
|
||||||
|
setTimeout(() => {
|
||||||
|
launchBot();
|
||||||
|
}, restartTimeout * 1000);
|
||||||
|
});
|
||||||
|
bot.start();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log(`ERROR: getting bot config. Restarting in ${restartTimeout} sec.`);
|
||||||
|
setTimeout(() => {
|
||||||
|
launchBot();
|
||||||
|
}, restartTimeout * 1000);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log(`ERROR: authorization. Restarting in ${restartTimeout} sec.`);
|
||||||
|
setTimeout(() => {
|
||||||
|
launchBot();
|
||||||
|
}, restartTimeout * 1000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
launchBot();
|
||||||
|
});
|
||||||
5
CMyTapper/robucks-bot/src/types/express.type.ts
Normal file
5
CMyTapper/robucks-bot/src/types/express.type.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
export interface TypedRequestBody<T> extends Request {
|
||||||
|
body: T;
|
||||||
|
}
|
||||||
14
CMyTapper/robucks-bot/src/utils/GetDifferenceInDays.ts
Normal file
14
CMyTapper/robucks-bot/src/utils/GetDifferenceInDays.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export function getDifferenceInDays(currentDate: Date, dateLifeTimeJwt: string | undefined) {
|
||||||
|
if (!dateLifeTimeJwt) {
|
||||||
|
return 31;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateLifeTime = new Date(dateLifeTimeJwt);
|
||||||
|
const oneDay = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const diffInTime = dateLifeTime.getTime() - currentDate.getTime();
|
||||||
|
|
||||||
|
const diffInDays = Math.round(Math.abs(diffInTime) / oneDay);
|
||||||
|
|
||||||
|
return diffInDays;
|
||||||
|
}
|
||||||
48
CMyTapper/robucks-bot/src/utils/HttpsInstanceFactory.ts
Normal file
48
CMyTapper/robucks-bot/src/utils/HttpsInstanceFactory.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||||
|
|
||||||
|
const BASE_URL = process.env.STRAPI_URL;
|
||||||
|
|
||||||
|
export class HttpInstanceFactory {
|
||||||
|
private static baseInstance: AxiosInstance | null = null;
|
||||||
|
private static authorizedInstance: AxiosInstance | null = null;
|
||||||
|
|
||||||
|
public static getBaseInstance(): AxiosInstance {
|
||||||
|
if (this.baseInstance) return this.baseInstance;
|
||||||
|
this.baseInstance = axios.create({
|
||||||
|
baseURL: BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return this.baseInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getAuthorizedInstance(token?: string): AxiosInstance {
|
||||||
|
if ((!this.authorizedInstance && token) || token) {
|
||||||
|
this.authorizedInstance = axios.create({
|
||||||
|
baseURL: BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.authorizedInstance) {
|
||||||
|
return this.authorizedInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getBaseInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async updateAuthorizedInstance() {
|
||||||
|
const res = await this.getBaseInstance().post('auth/local', {
|
||||||
|
identifier: process.env.STRAPI_USER ?? '',
|
||||||
|
password: process.env.STRAPI_PASSWORD ?? '',
|
||||||
|
});
|
||||||
|
HttpInstanceFactory.getAuthorizedInstance(res.data.jwt);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
CMyTapper/robucks-bot/src/utils/encrypter.ts
Normal file
6
CMyTapper/robucks-bot/src/utils/encrypter.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import crypto from 'crypto-js';
|
||||||
|
|
||||||
|
export const encryptId = (id: string): string => {
|
||||||
|
const secret = String(process.env.CRYPTO_SECRET);
|
||||||
|
return crypto.AES.encrypt(id, secret).toString();
|
||||||
|
};
|
||||||
14
CMyTapper/robucks-bot/tsconfig.json
Normal file
14
CMyTapper/robucks-bot/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["es6"],
|
||||||
|
"allowJs": true,
|
||||||
|
"outDir": "build",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
||||||
2080
CMyTapper/robucks-bot/yarn.lock
Normal file
2080
CMyTapper/robucks-bot/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
13
CMyTapper/robucks-front/.env
Normal file
13
CMyTapper/robucks-front/.env
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#Strapi
|
||||||
|
NEXT_PUBLIC_STRAPI_USER="admin"
|
||||||
|
NEXT_PUBLIC_STRAPI_PASSWORD="Admin1234"
|
||||||
|
|
||||||
|
#Prod
|
||||||
|
#NEXT_PUBLIC_STRAPI_URL="https://strapi.rbx-click.ru/api"
|
||||||
|
#NEXT_PUBLIC_STRAPI="https://strapi.rbx-click.ru"
|
||||||
|
#NEXT_PUBLIC_BOT_API="https://bot.rbx-click.ru"
|
||||||
|
|
||||||
|
#Local
|
||||||
|
NEXT_PUBLIC_STRAPI_URL="/api/strapi"
|
||||||
|
NEXT_PUBLIC_STRAPI=""
|
||||||
|
NEXT_PUBLIC_BOT_API="/api/bot"
|
||||||
24
CMyTapper/robucks-front/.eslintrc
Normal file
24
CMyTapper/robucks-front/.eslintrc
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": ["@typescript-eslint", "prettier"],
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": ["warn", {
|
||||||
|
"endOfLine": "auto",
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"useTabs": false,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": "warn",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"prefer-const": "warn",
|
||||||
|
"no-console": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
CMyTapper/robucks-front/.github/workflows/prod.yaml
vendored
Normal file
27
CMyTapper/robucks-front/.github/workflows/prod.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Deploy to prod
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy with pm2
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{secrets.PROD_SSH_HOST}}
|
||||||
|
username: ${{secrets.PROD_SSH_USERNAME}}
|
||||||
|
password: ${{secrets.PROD_SSH_PASSWORD}}
|
||||||
|
script_stop: true
|
||||||
|
script: |
|
||||||
|
export NVM_DIR=~/.nvm
|
||||||
|
source ~/.nvm/nvm.sh
|
||||||
|
cd ${{secrets.PROD_PROJECT_PATH}}
|
||||||
|
git checkout main
|
||||||
|
git pull
|
||||||
|
yarn install
|
||||||
|
pm2 restart ecosystem.config.js
|
||||||
38
CMyTapper/robucks-front/.gitignore
vendored
Normal file
38
CMyTapper/robucks-front/.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
.idea
|
||||||
36
CMyTapper/robucks-front/README.md
Normal file
36
CMyTapper/robucks-front/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/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/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||||
|
|
||||||
|
## 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/deployment) for more details.
|
||||||
40
CMyTapper/robucks-front/api/archive-request.api.ts
Normal file
40
CMyTapper/robucks-front/api/archive-request.api.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { AxiosInstance } from 'axios';
|
||||||
|
import { HttpInstanceFactory } from '@/utils/HttpInstanceFactory';
|
||||||
|
import { IPlayer } from '@/interfaces/player.type';
|
||||||
|
import { IExchangeRequest } from '@/api/exchange-request.api';
|
||||||
|
|
||||||
|
export interface IArchiveRequest {
|
||||||
|
id: number;
|
||||||
|
attributes: {
|
||||||
|
player: IPlayer;
|
||||||
|
exchange_requests: { data: IExchangeRequest[] };
|
||||||
|
rejected_requests: { data: IExchangeRequest[] };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ArchiveRequestApi {
|
||||||
|
private static instance: ArchiveRequestApi | null = null;
|
||||||
|
private httpInstance: AxiosInstance;
|
||||||
|
|
||||||
|
private constructor(token: string) {
|
||||||
|
this.httpInstance = HttpInstanceFactory.updateAuthorizedInstance(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(token: string): ArchiveRequestApi {
|
||||||
|
if (this.instance) return this.instance;
|
||||||
|
this.instance = new ArchiveRequestApi(token);
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createArchiveRequest(playerId: number): Promise<IArchiveRequest> {
|
||||||
|
return (
|
||||||
|
await this.httpInstance.post<IArchiveRequest>(`/archive-requests`, {
|
||||||
|
data: {
|
||||||
|
player: {
|
||||||
|
connect: [{ id: playerId }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
CMyTapper/robucks-front/api/auth.api.ts
Normal file
34
CMyTapper/robucks-front/api/auth.api.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { AxiosInstance } from 'axios';
|
||||||
|
import { HttpInstanceFactory } from '../utils/HttpInstanceFactory';
|
||||||
|
|
||||||
|
export interface IUser {
|
||||||
|
jwt: string;
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
confirmed: boolean;
|
||||||
|
blocked: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthApi {
|
||||||
|
private static instance: AuthApi | null = null;
|
||||||
|
private httpInstance: AxiosInstance;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.httpInstance = HttpInstanceFactory.getBaseInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): AuthApi {
|
||||||
|
if (this.instance) return this.instance;
|
||||||
|
this.instance = new AuthApi();
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(): Promise<IUser> {
|
||||||
|
return (await this.httpInstance.post<IUser>(`/auth/local`, {
|
||||||
|
identifier: process.env.NEXT_PUBLIC_STRAPI_USER,
|
||||||
|
password: process.env.NEXT_PUBLIC_STRAPI_PASSWORD,
|
||||||
|
})).data;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
CMyTapper/robucks-front/api/config-bot.api.ts
Normal file
42
CMyTapper/robucks-front/api/config-bot.api.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
|
||||||
|
export interface IConfigBot {
|
||||||
|
token: string;
|
||||||
|
dateLifetimeJwt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBotLink {
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConfigBotApi {
|
||||||
|
private static instance: ConfigBotApi | null = null;
|
||||||
|
private httpBotInstance: AxiosInstance;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
const isServer = typeof window === 'undefined';
|
||||||
|
const baseURL = isServer
|
||||||
|
? 'http://127.0.0.1:3001'
|
||||||
|
: process.env.NEXT_PUBLIC_BOT_API;
|
||||||
|
this.httpBotInstance = axios.create({
|
||||||
|
baseURL,
|
||||||
|
headers: {
|
||||||
|
'ngrok-skip-browser-warning': 'true',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): ConfigBotApi {
|
||||||
|
if (this.instance) return this.instance;
|
||||||
|
this.instance = new ConfigBotApi();
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfigBot(): Promise<IConfigBot> {
|
||||||
|
return (await this.httpBotInstance.get<IConfigBot>(`/config-bot-data`)).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBotLink(): Promise<IBotLink> {
|
||||||
|
return (await this.httpBotInstance.get<IBotLink>(`/link-bot`)).data;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
CMyTapper/robucks-front/api/config.api.ts
Normal file
33
CMyTapper/robucks-front/api/config.api.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { AxiosInstance } from 'axios';
|
||||||
|
import { HttpInstanceFactory } from '@/utils/HttpInstanceFactory';
|
||||||
|
import { IPlayer } from '@/interfaces/player.type';
|
||||||
|
|
||||||
|
export interface IConfig {
|
||||||
|
id: number;
|
||||||
|
attributes: {
|
||||||
|
farm_amount: number;
|
||||||
|
farming_time: number;
|
||||||
|
admins: { data: IPlayer[] };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConfigApi {
|
||||||
|
private static instance: ConfigApi | null = null;
|
||||||
|
private httpInstance: AxiosInstance;
|
||||||
|
|
||||||
|
private constructor(token: string, shouldUpdateInstance: boolean = false) {
|
||||||
|
this.httpInstance = HttpInstanceFactory.getAuthorizedInstance(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(token: string, shouldUpdateInstance: boolean = false): ConfigApi {
|
||||||
|
if (!shouldUpdateInstance && this.instance) {
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
this.instance = new ConfigApi(token);
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfig(): Promise<{ data: IConfig }> {
|
||||||
|
return (await this.httpInstance.get<{ data: IConfig }>(`/config?populate=*`)).data;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
CMyTapper/robucks-front/api/exchange-request.api.ts
Normal file
82
CMyTapper/robucks-front/api/exchange-request.api.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import { HttpInstanceFactory } from '@/utils/HttpInstanceFactory';
|
||||||
|
import { IPlayer } from '@/interfaces/player.type';
|
||||||
|
import { toFixed } from '@/utils/FormatNumber';
|
||||||
|
|
||||||
|
export interface IExchangeCreate {
|
||||||
|
amount: number;
|
||||||
|
total: number;
|
||||||
|
playerId: number;
|
||||||
|
nickname: string;
|
||||||
|
balance: number;
|
||||||
|
telegram_id: string[] | number[];
|
||||||
|
shop_item?: number;
|
||||||
|
archive_request_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExchangeRequest {
|
||||||
|
id: number;
|
||||||
|
attributes: {
|
||||||
|
amount: number;
|
||||||
|
total: number;
|
||||||
|
player: { data: IPlayer };
|
||||||
|
roblox_nick: string;
|
||||||
|
status: 'done' | 'rejected' | 'pending';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExchangeRequestApi {
|
||||||
|
private static instance: ExchangeRequestApi | null = null;
|
||||||
|
private httpInstance: AxiosInstance;
|
||||||
|
private httpBotInstance: AxiosInstance;
|
||||||
|
|
||||||
|
private constructor(token: string) {
|
||||||
|
this.httpInstance = HttpInstanceFactory.updateAuthorizedInstance(token);
|
||||||
|
this.httpBotInstance = axios.create({
|
||||||
|
baseURL: process.env.NEXT_PUBLIC_BOT_API,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(token: string): ExchangeRequestApi {
|
||||||
|
if (this.instance) return this.instance;
|
||||||
|
this.instance = new ExchangeRequestApi(token);
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createExchange(data: IExchangeCreate): Promise<IExchangeRequest> {
|
||||||
|
const res = (
|
||||||
|
await this.httpInstance.post<{ data: IExchangeRequest }>(
|
||||||
|
`/exchange-requests?populate=*`,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
amount: toFixed(data.amount),
|
||||||
|
total: toFixed(data.total),
|
||||||
|
roblox_nick: data.nickname,
|
||||||
|
status: 'pending',
|
||||||
|
shop_item: data.shop_item ? data.shop_item : null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).data.data;
|
||||||
|
if (res) {
|
||||||
|
await this.httpInstance.put(`/archive-requests/${data.archive_request_id}`, {
|
||||||
|
data: {
|
||||||
|
exchange_requests: {
|
||||||
|
connect: [res.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await this.httpInstance.put(`/players/${data.playerId}`, {
|
||||||
|
data: { balance: toFixed(data.balance - data.amount) },
|
||||||
|
});
|
||||||
|
await this.httpBotInstance.post(`/notify`, {
|
||||||
|
telegram_id: data.telegram_id,
|
||||||
|
exchange_id: res.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
CMyTapper/robucks-front/api/player.api.ts
Normal file
56
CMyTapper/robucks-front/api/player.api.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { AxiosInstance } from 'axios';
|
||||||
|
import { HttpInstanceFactory } from '@/utils/HttpInstanceFactory';
|
||||||
|
import { IPlayer, IUpdatePlayerTask } from '@/interfaces/player.type';
|
||||||
|
|
||||||
|
export class PlayerApi {
|
||||||
|
private static instance: PlayerApi | null = null;
|
||||||
|
private httpInstance: AxiosInstance;
|
||||||
|
private populateString: string =
|
||||||
|
'populate=archive_request.rejected_requests&populate=archive_request.exchange_requests&populate=finished_tasks.task.icon&populate=referral.my_referrals&populate=my_referrals.my_referrals';
|
||||||
|
|
||||||
|
private constructor(token?: string) {
|
||||||
|
this.httpInstance = HttpInstanceFactory.getAuthorizedInstance(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(token?: string): PlayerApi {
|
||||||
|
if (this.instance) return this.instance;
|
||||||
|
this.instance = new PlayerApi(token);
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findPlayerByAuthId(authId: string): Promise<IPlayer | null> {
|
||||||
|
const filteredPlayers: IPlayer[] = (
|
||||||
|
await this.httpInstance.get(
|
||||||
|
`/players/?filters[auth_id]=${authId}&${this.populateString}`,
|
||||||
|
)
|
||||||
|
).data.data;
|
||||||
|
return filteredPlayers.length > 0 ? filteredPlayers[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async startFarm(startFarm: Date, playerId: number): Promise<IPlayer> {
|
||||||
|
return (
|
||||||
|
await this.httpInstance.put<{ data: IPlayer }>(
|
||||||
|
`/players/${playerId}?${this.populateString}`,
|
||||||
|
{
|
||||||
|
data: { start_farm: startFarm },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async claimRewards(playerId: number, balance: number): Promise<IPlayer> {
|
||||||
|
return (
|
||||||
|
await this.httpInstance.put<{ data: IPlayer }>(
|
||||||
|
`/players/${playerId}?${this.populateString}`,
|
||||||
|
{
|
||||||
|
data: { balance: balance, start_farm: null },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTasks(playerId: number, data: IUpdatePlayerTask): Promise<IPlayer> {
|
||||||
|
return (await this.httpInstance.put<IPlayer>(`/players/finished-tasks/${playerId}`, data))
|
||||||
|
.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
CMyTapper/robucks-front/api/shop-item.api.ts
Normal file
39
CMyTapper/robucks-front/api/shop-item.api.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { AxiosInstance } from 'axios';
|
||||||
|
import { HttpInstanceFactory } from '@/utils/HttpInstanceFactory';
|
||||||
|
|
||||||
|
export interface IShopItem {
|
||||||
|
id: number;
|
||||||
|
attributes: {
|
||||||
|
isDecimal: boolean;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
icon: {
|
||||||
|
data: {
|
||||||
|
id: number;
|
||||||
|
attributes: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShopItemApi {
|
||||||
|
private static instance: ShopItemApi | null = null;
|
||||||
|
private httpInstance: AxiosInstance;
|
||||||
|
|
||||||
|
private constructor(token?: string) {
|
||||||
|
this.httpInstance = HttpInstanceFactory.getAuthorizedInstance(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(token?: string): ShopItemApi {
|
||||||
|
if (this.instance) return this.instance;
|
||||||
|
this.instance = new ShopItemApi(token);
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllShopItems(): Promise<IShopItem[]> {
|
||||||
|
return (await this.httpInstance.get<{ data: IShopItem[] }>(`/shop-items?populate=*`)).data
|
||||||
|
.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
CMyTapper/robucks-front/api/tasks.api.ts
Normal file
38
CMyTapper/robucks-front/api/tasks.api.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { AxiosInstance } from 'axios';
|
||||||
|
import { HttpInstanceFactory } from '@/utils/HttpInstanceFactory';
|
||||||
|
|
||||||
|
export interface ITask {
|
||||||
|
id: number;
|
||||||
|
attributes: {
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
prize: number;
|
||||||
|
icon: {
|
||||||
|
data: {
|
||||||
|
id: number;
|
||||||
|
attributes: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TasksApi {
|
||||||
|
private static instance: TasksApi | null = null;
|
||||||
|
private httpInstance: AxiosInstance;
|
||||||
|
|
||||||
|
private constructor(token?: string) {
|
||||||
|
this.httpInstance = HttpInstanceFactory.getAuthorizedInstance(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(token?: string): TasksApi {
|
||||||
|
if (this.instance) return this.instance;
|
||||||
|
this.instance = new TasksApi(token);
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllTasks(): Promise<ITask[]> {
|
||||||
|
return (await this.httpInstance.get<{ data: ITask[] }>(`/tasks?populate=*`)).data.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
CMyTapper/robucks-front/app/[id]/actions.ts
Normal file
10
CMyTapper/robucks-front/app/[id]/actions.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
'use server';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
|
export async function setNewToken(token: string) {
|
||||||
|
cookies().set('token', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setNowDate() {
|
||||||
|
cookies().set('dateLifetimeJwt', new Date().toDateString());
|
||||||
|
}
|
||||||
11
CMyTapper/robucks-front/app/[id]/friends/page.tsx
Normal file
11
CMyTapper/robucks-front/app/[id]/friends/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import FriendsContainer from '@/components/friends/FriendsContiner';
|
||||||
|
import { ConfigBotApi } from '@/api/config-bot.api';
|
||||||
|
|
||||||
|
const page = async () => {
|
||||||
|
const configBotApi = ConfigBotApi.getInstance();
|
||||||
|
const botLink = await configBotApi.getBotLink();
|
||||||
|
|
||||||
|
return <FriendsContainer botLink={botLink.link} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
7
CMyTapper/robucks-front/app/[id]/home/page.tsx
Normal file
7
CMyTapper/robucks-front/app/[id]/home/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import HomeContainer from '@/components/home/HomeContainer';
|
||||||
|
|
||||||
|
const page = async () => {
|
||||||
|
return <HomeContainer />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
13
CMyTapper/robucks-front/app/[id]/layout.tsx
Normal file
13
CMyTapper/robucks-front/app/[id]/layout.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import Wrapper from '@/components/ui/wrapper/Wrapper';
|
||||||
|
import { Providers } from '@/components/providers/Providers';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import { Params } from 'next/dist/shared/lib/router/utils/route-matcher';
|
||||||
|
|
||||||
|
export default function RootLayout({ children, params }: PropsWithChildren & { params: Params }) {
|
||||||
|
const id = decodeURI(params.id);
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<Providers id={id}>{children}</Providers>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
CMyTapper/robucks-front/app/[id]/page.tsx
Normal file
5
CMyTapper/robucks-front/app/[id]/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const page = async () => {
|
||||||
|
return <>hello</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
7
CMyTapper/robucks-front/app/[id]/shop/page.tsx
Normal file
7
CMyTapper/robucks-front/app/[id]/shop/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import ShopContainer from '@/components/shop/ShopContainer';
|
||||||
|
|
||||||
|
const page = async () => {
|
||||||
|
return <ShopContainer />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
7
CMyTapper/robucks-front/app/[id]/tasks/page.tsx
Normal file
7
CMyTapper/robucks-front/app/[id]/tasks/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import TasksContainer from '@/components/tasks/TasksContainer';
|
||||||
|
|
||||||
|
const page = async () => {
|
||||||
|
return <TasksContainer />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
51
CMyTapper/robucks-front/app/api/bot/[...path]/route.ts
Normal file
51
CMyTapper/robucks-front/app/api/bot/[...path]/route.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const BOT_API_URL = process.env.BOT_API_INTERNAL || 'http://127.0.0.1:3001';
|
||||||
|
|
||||||
|
async function proxyRequest(req: NextRequest, params: { path: string[] }) {
|
||||||
|
const path = params.path.join('/');
|
||||||
|
const url = `${BOT_API_URL}/${path}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
method: req.method,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||||
|
const body = await req.text();
|
||||||
|
if (body) {
|
||||||
|
fetchOptions.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, fetchOptions);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
return NextResponse.json(data, { status: response.status });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Bot proxy error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Bot API unavailable' },
|
||||||
|
{ status: 502 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } }
|
||||||
|
) {
|
||||||
|
return proxyRequest(req, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } }
|
||||||
|
) {
|
||||||
|
return proxyRequest(req, params);
|
||||||
|
}
|
||||||
71
CMyTapper/robucks-front/app/api/strapi/[...path]/route.ts
Normal file
71
CMyTapper/robucks-front/app/api/strapi/[...path]/route.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const STRAPI_URL = process.env.STRAPI_INTERNAL || 'http://127.0.0.1:1337/api';
|
||||||
|
|
||||||
|
async function proxyRequest(req: NextRequest, params: { path: string[] }) {
|
||||||
|
const path = params.path.join('/');
|
||||||
|
const searchParams = req.nextUrl.searchParams.toString();
|
||||||
|
const url = `${STRAPI_URL}/${path}${searchParams ? `?${searchParams}` : ''}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
const authHeader = req.headers.get('Authorization');
|
||||||
|
if (authHeader) {
|
||||||
|
headers['Authorization'] = authHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
method: req.method,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||||
|
const body = await req.text();
|
||||||
|
if (body) {
|
||||||
|
fetchOptions.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, fetchOptions);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
return NextResponse.json(data, { status: response.status });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Strapi proxy error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Strapi API unavailable' },
|
||||||
|
{ status: 502 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } }
|
||||||
|
) {
|
||||||
|
return proxyRequest(req, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } }
|
||||||
|
) {
|
||||||
|
return proxyRequest(req, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } }
|
||||||
|
) {
|
||||||
|
return proxyRequest(req, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } }
|
||||||
|
) {
|
||||||
|
return proxyRequest(req, params);
|
||||||
|
}
|
||||||
BIN
CMyTapper/robucks-front/app/favicon.ico
Normal file
BIN
CMyTapper/robucks-front/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
78
CMyTapper/robucks-front/app/layout.tsx
Normal file
78
CMyTapper/robucks-front/app/layout.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import Script from 'next/script';
|
||||||
|
import localFont from 'next/font/local';
|
||||||
|
import { Manrope } from 'next/font/google';
|
||||||
|
|
||||||
|
import '../public/globals.css';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import { Params } from 'next/dist/shared/lib/router/utils/route-matcher';
|
||||||
|
|
||||||
|
const manrope = Manrope({
|
||||||
|
weight: ['200', '300', '400', '500', '600', '700'],
|
||||||
|
style: ['normal'],
|
||||||
|
subsets: ['latin'],
|
||||||
|
variable: '--font-manrope',
|
||||||
|
});
|
||||||
|
|
||||||
|
const clashFont = localFont({
|
||||||
|
src: [
|
||||||
|
{
|
||||||
|
path: '../public/fonts/ClashDisplay-Bold.otf',
|
||||||
|
weight: '800',
|
||||||
|
style: 'normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '../public/fonts/ClashDisplay-Extralight.otf',
|
||||||
|
weight: '100',
|
||||||
|
style: 'normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '../public/fonts/ClashDisplay-Light.otf',
|
||||||
|
weight: '200',
|
||||||
|
style: 'normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '../public/fonts/ClashDisplay-Regular.otf',
|
||||||
|
weight: '400',
|
||||||
|
style: 'normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '../public/fonts/ClashDisplay-Semibold.otf',
|
||||||
|
weight: '600',
|
||||||
|
style: 'normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '../public/fonts/ClashDisplay-Medium.otf',
|
||||||
|
weight: '500',
|
||||||
|
style: 'normal',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variable: '--font-clash',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Robucks clicker',
|
||||||
|
description: 'Robucks clicker',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: PropsWithChildren & { params: Params }) {
|
||||||
|
return (
|
||||||
|
<html lang='ru'>
|
||||||
|
<head>
|
||||||
|
<Script
|
||||||
|
src='https://telegram.org/js/telegram-web-app.js'
|
||||||
|
strategy='beforeInteractive'
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body
|
||||||
|
className={`${clashFont.className} ${manrope.variable}`}
|
||||||
|
suppressHydrationWarning={true}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
CMyTapper/robucks-front/app/page.tsx
Normal file
3
CMyTapper/robucks-front/app/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <span>Not found</span>;
|
||||||
|
}
|
||||||
34
CMyTapper/robucks-front/components/friends/Friend.module.css
Normal file
34
CMyTapper/robucks-front/components/friends/Friend.module.css
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
.friend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: #FFFFFF0D;
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
height: 74px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friendContent {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friendData {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friendReferrals {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friendBalance span {
|
||||||
|
font-family: var(--font-manrope);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
43
CMyTapper/robucks-front/components/friends/Friend.tsx
Normal file
43
CMyTapper/robucks-front/components/friends/Friend.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
'use client';
|
||||||
|
import styles from '@/components/friends/Friend.module.css';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { IPlayer } from '@/interfaces/player.type';
|
||||||
|
import { toFixed } from '@/utils/FormatNumber';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
player: IPlayer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Friend = ({ player }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.friend}>
|
||||||
|
<div className={styles.friendContent}>
|
||||||
|
<div className={styles.friendAvatar}>
|
||||||
|
<Image
|
||||||
|
src={'/icons/friend-icon.svg'}
|
||||||
|
alt={'friend avatar'}
|
||||||
|
height={42}
|
||||||
|
width={42}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.friendData}>
|
||||||
|
<span>{player?.attributes.telegram_nick}</span>
|
||||||
|
<div className={styles.friendReferrals}>
|
||||||
|
<Image
|
||||||
|
src={'/icons/referrals-icon.svg'}
|
||||||
|
alt={'referrals icon'}
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
/>
|
||||||
|
<span>{player?.attributes.my_referrals?.data?.length || 0}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.friendBalance}>
|
||||||
|
<span>{toFixed(player?.attributes.balance)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Friend;
|
||||||
113
CMyTapper/robucks-front/components/friends/Friends.module.css
Normal file
113
CMyTapper/robucks-front/components/friends/Friends.module.css
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
.friends {
|
||||||
|
background: #101010;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-bottom: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friendsTitle {
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-top: 45px;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 19px;
|
||||||
|
color: white;
|
||||||
|
line-height: 21px;
|
||||||
|
font-family: var(--font-manrope);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inviteBlock {
|
||||||
|
width: min(343px, 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 20px 16px;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
background: linear-gradient(81.19deg, rgba(66, 187, 71, 0.06) 0%, rgba(66, 187, 71, 0.2) 100%), #0E0E10;
|
||||||
|
border: 1px solid #18181A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rewardInvite {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
gap: 3px;
|
||||||
|
font-family: var(--font-clash);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rewardInvite span {
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 51px;
|
||||||
|
background: linear-gradient(180deg, #42BB47 4.41%, #33A137 61.59%, #42BB47 101.32%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inviteButtonBlock {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inviteButtonBlock button {
|
||||||
|
width: 100%;
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 40px;
|
||||||
|
color: white;
|
||||||
|
background: #42BB47;
|
||||||
|
border: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 21px;
|
||||||
|
font-family: var(--font-manrope);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoInvite {
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-manrope);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoInvite span {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #BBBBBB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friendsList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 1rem;
|
||||||
|
margin-top: 7px;
|
||||||
|
gap: 5px;
|
||||||
|
padding-bottom: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friendsListBlock {
|
||||||
|
width: min(375px, 100%);
|
||||||
|
margin-top: 2rem;
|
||||||
|
background: #0E0E10;
|
||||||
|
padding: 20px 16px;
|
||||||
|
border: 1px solid #18181A;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friendsListHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 19px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: var(--font-manrope);
|
||||||
|
line-height: 19px;
|
||||||
|
}
|
||||||
67
CMyTapper/robucks-front/components/friends/Friends.tsx
Normal file
67
CMyTapper/robucks-front/components/friends/Friends.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
'use client';
|
||||||
|
import styles from '@/components/friends/Friends.module.css';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import Friend from '@/components/friends/Friend';
|
||||||
|
import CopyLinkToast from '@/components/ui/copy-link-toast/CopyLinkToast';
|
||||||
|
import { usePlayerStore } from '@/store/player.state';
|
||||||
|
|
||||||
|
interface IFriends {
|
||||||
|
isVisible: boolean;
|
||||||
|
referral_fee: number | undefined;
|
||||||
|
friendsAndReferralsCount: { friendsWord: string; referralWord: string };
|
||||||
|
handleCopyInviteLink: () => Promise<void>;
|
||||||
|
handleCloseToastLink: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Friends: FC<IFriends> = ({
|
||||||
|
isVisible,
|
||||||
|
referral_fee,
|
||||||
|
friendsAndReferralsCount,
|
||||||
|
handleCopyInviteLink,
|
||||||
|
handleCloseToastLink,
|
||||||
|
}) => {
|
||||||
|
const { player } = usePlayerStore();
|
||||||
|
const referrals = player?.attributes.my_referrals.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.friends}>
|
||||||
|
<div className={styles.friendsTitle}>
|
||||||
|
<span>Друзья</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.inviteBlock}>
|
||||||
|
<div className={styles.rewardInvite}>
|
||||||
|
<span>{referral_fee || 0}</span>
|
||||||
|
<Image
|
||||||
|
src={'/icons/robucks-icon.svg'}
|
||||||
|
alt={'robucks icon'}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.inviteButtonBlock}>
|
||||||
|
<button onClick={handleCopyInviteLink}>Пригласи друга</button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.infoInvite}>
|
||||||
|
<span>
|
||||||
|
Получай 10% от твоих друзей +5% от друзей реферала +2.5% от их рефералов
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.friendsListBlock}>
|
||||||
|
<div className={styles.friendsListHeader}>
|
||||||
|
<span>{friendsAndReferralsCount.friendsWord}</span>
|
||||||
|
<span>{friendsAndReferralsCount.referralWord}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.friendsList}>
|
||||||
|
{referrals?.map(referral => <Friend key={referral.id} player={referral} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CopyLinkToast isVisible={isVisible} handleCloseToastLink={handleCloseToastLink} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Friends;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Friends from '@/components/friends/Friends';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { CalculateReferralsCount } from '@/utils/CalculateReferralsCount';
|
||||||
|
import { updateFriendAndReferralCounts } from '@/utils/UpdateFriendAndReferralCount';
|
||||||
|
import { usePlayerStore } from '@/store/player.state';
|
||||||
|
|
||||||
|
const FriendsContainer = ({ botLink }: { botLink: string }) => {
|
||||||
|
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [userData, setUserData] = useState<{ id: number; username: string } | null>(null);
|
||||||
|
|
||||||
|
const { player } = usePlayerStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initDataUnsafe = window.Telegram?.WebApp?.initDataUnsafe;
|
||||||
|
if (!initDataUnsafe?.user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setUserData(initDataUnsafe.user);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCopyInviteLink = useCallback(async () => {
|
||||||
|
setIsVisible(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
}, 3000);
|
||||||
|
return await navigator.clipboard.writeText(`${botLink}ref_${userData?.id}`);
|
||||||
|
}, [userData, botLink]);
|
||||||
|
|
||||||
|
const handleCloseToastLink = useCallback(() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Friends
|
||||||
|
isVisible={isVisible}
|
||||||
|
referral_fee={player?.attributes.referral_fee}
|
||||||
|
friendsAndReferralsCount={updateFriendAndReferralCounts(
|
||||||
|
player?.attributes.my_referrals?.data?.length || 0,
|
||||||
|
CalculateReferralsCount(player),
|
||||||
|
)}
|
||||||
|
handleCopyInviteLink={handleCopyInviteLink}
|
||||||
|
handleCloseToastLink={handleCloseToastLink}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FriendsContainer;
|
||||||
137
CMyTapper/robucks-front/components/home/Home.module.css
Normal file
137
CMyTapper/robucks-front/components/home/Home.module.css
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
.home {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-image: url("../../public/images/background-img.jpg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
height: 100%;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greeting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 34.4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greeting span {
|
||||||
|
background: linear-gradient(45deg, #42BB47, #33A137, #42BB47);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balanceAmount {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totalBalance {
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 51px;
|
||||||
|
background: linear-gradient(45deg, #42BB47, #33A137, #42BB47);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.miningButton {
|
||||||
|
margin-bottom: 75px;
|
||||||
|
width: 187px;
|
||||||
|
height: 60px;
|
||||||
|
padding: 20px 30px;
|
||||||
|
background: #42BB47;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 19.6px;
|
||||||
|
box-shadow: 0 0 42px 0 #42BB47B2;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: 0.5px solid;
|
||||||
|
cursor: pointer;
|
||||||
|
border-image-source: linear-gradient(180deg, #42BB47 4.41%, #308A34 61.59%, #42BB47 101.32%);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.farmingButton {
|
||||||
|
margin-bottom: 75px;
|
||||||
|
width: 300px;
|
||||||
|
height: 60px;
|
||||||
|
padding: 20px 30px;
|
||||||
|
background: black;
|
||||||
|
box-shadow: 0px 0px 42px 0px #42BB4780;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: 1px solid #42BB4780;
|
||||||
|
color: #42BB4780;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 19.6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claimReward {
|
||||||
|
box-shadow: 0px 0px 42px 0px #42BB4780;
|
||||||
|
width: 257px;
|
||||||
|
height: 60px;
|
||||||
|
padding: 20px 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 75px;
|
||||||
|
border-radius: 30px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 19.6px;
|
||||||
|
background: #10101080;
|
||||||
|
border: 1px solid #42BB47;
|
||||||
|
color: #42BB4780;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claimAmount {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claimAmount span {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #42BB47;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward {
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: #42BB47;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward span {
|
||||||
|
color: #42BB47;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabledButton {
|
||||||
|
background: #10101080;
|
||||||
|
color: #42BB47;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.miningButton,
|
||||||
|
.farmingButton,
|
||||||
|
.claimReward {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
62
CMyTapper/robucks-front/components/home/Home.tsx
Normal file
62
CMyTapper/robucks-front/components/home/Home.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import styles from './Home.module.css';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { IConfig } from '@/api/config.api';
|
||||||
|
import ButtonMining from '@/components/ui/button-mining/ButtonMining';
|
||||||
|
import { usePlayerStore } from '@/store/player.state';
|
||||||
|
import { toFixed } from '@/utils/FormatNumber';
|
||||||
|
|
||||||
|
interface IHome {
|
||||||
|
remainingTime: string | null;
|
||||||
|
config: IConfig | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
typeButton: 'pending' | 'farming' | 'claim_reward';
|
||||||
|
handleStartFarming: () => void;
|
||||||
|
handleClaimRewards: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Home: FC<IHome> = ({
|
||||||
|
remainingTime,
|
||||||
|
config,
|
||||||
|
isLoading,
|
||||||
|
typeButton,
|
||||||
|
handleStartFarming,
|
||||||
|
handleClaimRewards,
|
||||||
|
}) => {
|
||||||
|
const { player } = usePlayerStore();
|
||||||
|
return (
|
||||||
|
<div className={styles.home}>
|
||||||
|
<div className={styles.main}>
|
||||||
|
<div className={styles.greeting}>
|
||||||
|
<p>Привет, </p>
|
||||||
|
<span>{player ? player.attributes.telegram_nick : '-'}!</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.balance}>
|
||||||
|
<span>Ваш баланс</span>
|
||||||
|
<div className={styles.balanceAmount}>
|
||||||
|
<Image
|
||||||
|
src={'/icons/robucks-icon.svg'}
|
||||||
|
alt={'robucks icon'}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
<span className={styles.totalBalance}>
|
||||||
|
{player ? toFixed(player.attributes.balance) : '0'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ButtonMining
|
||||||
|
isLoading={isLoading}
|
||||||
|
typeButton={typeButton}
|
||||||
|
remainingTime={remainingTime}
|
||||||
|
farm_amount={config?.attributes.farm_amount}
|
||||||
|
handleStartFarming={handleStartFarming}
|
||||||
|
handleClaimRewards={handleClaimRewards}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
||||||
93
CMyTapper/robucks-front/components/home/HomeContainer.tsx
Normal file
93
CMyTapper/robucks-front/components/home/HomeContainer.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import Home from '@/components/home/Home';
|
||||||
|
import { FormatTime } from '@/utils/FormatTime';
|
||||||
|
import { useButtonStore } from '@/store/button.state';
|
||||||
|
import { PlayerApi } from '@/api/player.api';
|
||||||
|
import { usePlayerStore } from '@/store/player.state';
|
||||||
|
import { useConfigStore } from '@/store/config.state';
|
||||||
|
import { useTokenStore } from '@/store/token.state';
|
||||||
|
import { toFixed } from '@/utils/FormatNumber';
|
||||||
|
|
||||||
|
const HomeContainer = () => {
|
||||||
|
const { typeButton, setTypeButton } = useButtonStore();
|
||||||
|
|
||||||
|
const { player: playerStore, setPlayer } = usePlayerStore();
|
||||||
|
const { config: configStore } = useConfigStore();
|
||||||
|
const { token } = useTokenStore();
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
const [remainingTime, setRemainingTime] = useState<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [isLoading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (playerStore && playerStore.attributes.start_farm && configStore) {
|
||||||
|
const farmingTime = configStore.attributes.farming_time * 1000;
|
||||||
|
const endTime = new Date(playerStore.attributes.start_farm).getTime() + farmingTime;
|
||||||
|
|
||||||
|
const updateRemainingTime = () => {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const timeLeft = endTime - now;
|
||||||
|
setRemainingTime(timeLeft > 0 ? timeLeft : 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateRemainingTime();
|
||||||
|
const interval = setInterval(updateRemainingTime, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
} else {
|
||||||
|
setRemainingTime(null);
|
||||||
|
}
|
||||||
|
}, [playerStore, configStore]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!remainingTime && !playerStore?.attributes.start_farm) {
|
||||||
|
setTypeButton('pending');
|
||||||
|
} else if (remainingTime && playerStore?.attributes.start_farm) {
|
||||||
|
setTypeButton('farming');
|
||||||
|
} else if (!remainingTime && playerStore?.attributes.start_farm) {
|
||||||
|
setTypeButton('claim_reward');
|
||||||
|
}
|
||||||
|
}, [remainingTime, playerStore]);
|
||||||
|
|
||||||
|
const handleStartFarming = useCallback(async () => {
|
||||||
|
if (!token || !playerStore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const api = PlayerApi.getInstance(token);
|
||||||
|
const updatedPlayer = await api.startFarm(new Date(), playerStore.id);
|
||||||
|
setPlayer(updatedPlayer);
|
||||||
|
}, [playerStore]);
|
||||||
|
|
||||||
|
const handleClaimRewards = useCallback(async () => {
|
||||||
|
if (!token || !playerStore || !configStore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const api = PlayerApi.getInstance(token);
|
||||||
|
// TODO: улучшить безопасность в методе claimRewards
|
||||||
|
const updatedPlayer = await api.claimRewards(
|
||||||
|
playerStore.id,
|
||||||
|
toFixed(playerStore?.attributes.balance) + configStore?.attributes.farm_amount,
|
||||||
|
);
|
||||||
|
setPlayer(updatedPlayer);
|
||||||
|
}, [playerStore, configStore]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Home
|
||||||
|
config={configStore}
|
||||||
|
isLoading={isLoading}
|
||||||
|
typeButton={typeButton}
|
||||||
|
remainingTime={FormatTime(remainingTime)}
|
||||||
|
handleStartFarming={handleStartFarming}
|
||||||
|
handleClaimRewards={handleClaimRewards}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomeContainer;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { PropsWithChildren, useEffect } from 'react';
|
||||||
|
import { useTokenStore } from '@/store/token.state';
|
||||||
|
import { ConfigApi } from '@/api/config.api';
|
||||||
|
import { useConfigStore } from '@/store/config.state';
|
||||||
|
|
||||||
|
export const ConfigProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const { setConfig } = useConfigStore();
|
||||||
|
const { token } = useTokenStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const api = ConfigApi.getInstance(token);
|
||||||
|
api.getConfig().then(config => setConfig(config.data));
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { PropsWithChildren, useEffect, useState } from 'react';
|
||||||
|
import { usePlayerStore } from '@/store/player.state';
|
||||||
|
import { ProvidersProps } from '@/components/providers/Providers';
|
||||||
|
import { useTokenStore } from '@/store/token.state';
|
||||||
|
import { PlayerApi } from '@/api/player.api';
|
||||||
|
import styles from './Providers.module.css';
|
||||||
|
|
||||||
|
export const PlayerProvider = ({
|
||||||
|
children,
|
||||||
|
id,
|
||||||
|
}: PropsWithChildren & Pick<ProvidersProps, 'id'>) => {
|
||||||
|
const { player, setPlayer, isLoading, stopLoading } = usePlayerStore();
|
||||||
|
const { token } = useTokenStore();
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token) {
|
||||||
|
console.log('[PlayerProvider] no token yet, waiting...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('[PlayerProvider] token received, loading player...');
|
||||||
|
try {
|
||||||
|
const tgUser = window.Telegram?.WebApp?.initDataUnsafe?.user;
|
||||||
|
const username = String(tgUser?.username || 'unknown');
|
||||||
|
console.log('[PlayerProvider] telegram username:', username, 'tgUser:', tgUser);
|
||||||
|
setUsername(username);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[PlayerProvider] error getting telegram user:', e);
|
||||||
|
}
|
||||||
|
const playerApi = PlayerApi.getInstance(token);
|
||||||
|
playerApi.findPlayerByAuthId(id).then(player => {
|
||||||
|
console.log('[PlayerProvider] player loaded:', player?.id);
|
||||||
|
setPlayer(player);
|
||||||
|
stopLoading();
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[PlayerProvider] error loading player:', err);
|
||||||
|
stopLoading();
|
||||||
|
});
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className={styles.loading}>
|
||||||
|
<span>Загрузка...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!player) {
|
||||||
|
return (
|
||||||
|
<div className={styles.loading}>
|
||||||
|
<span>
|
||||||
|
Что-то пошло не так... Попробуйте написать /start, чтобы получить актуальную
|
||||||
|
ссылку
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (player.attributes.telegram_nick !== username) {
|
||||||
|
return (
|
||||||
|
<div className={styles.loading}>
|
||||||
|
<span>
|
||||||
|
Похоже, что Вы изменили свой ID в Telegram, попробуйте написать /start, чтобы
|
||||||
|
получить актуальную ссылку
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.loading {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: black;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--font-manrope);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
28
CMyTapper/robucks-front/components/providers/Providers.tsx
Normal file
28
CMyTapper/robucks-front/components/providers/Providers.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import { PlayerProvider } from '@/components/providers/PlayerProvider';
|
||||||
|
import { TokenProvider } from '@/components/providers/TokenProvider';
|
||||||
|
import { TasksProvider } from '@/components/providers/TasksProvider';
|
||||||
|
import { ShopItemsProviver } from '@/components/providers/ShopItemsProvider';
|
||||||
|
import { ConfigProvider } from '@/components/providers/ConfigProvider';
|
||||||
|
import Navigation from '@/components/ui/navigation/Navigation';
|
||||||
|
|
||||||
|
export type ProvidersProps = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Providers = ({ children, id }: PropsWithChildren & ProvidersProps) => {
|
||||||
|
return (
|
||||||
|
<TokenProvider>
|
||||||
|
<TasksProvider>
|
||||||
|
<ShopItemsProviver>
|
||||||
|
<ConfigProvider>
|
||||||
|
<PlayerProvider id={id}>
|
||||||
|
{children}
|
||||||
|
<Navigation id={id} />
|
||||||
|
</PlayerProvider>
|
||||||
|
</ConfigProvider>
|
||||||
|
</ShopItemsProviver>
|
||||||
|
</TasksProvider>
|
||||||
|
</TokenProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { PropsWithChildren, useEffect } from 'react';
|
||||||
|
import { useTokenStore } from '@/store/token.state';
|
||||||
|
import { ShopItemApi } from '@/api/shop-item.api';
|
||||||
|
import { useShopItemsStore } from '@/store/shopItems.state';
|
||||||
|
|
||||||
|
export const ShopItemsProviver = ({ children }: PropsWithChildren) => {
|
||||||
|
const { setShopItems } = useShopItemsStore();
|
||||||
|
const { token } = useTokenStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const api = ShopItemApi.getInstance(token);
|
||||||
|
api.getAllShopItems().then(shopItems => setShopItems(shopItems));
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { PropsWithChildren, useEffect } from 'react';
|
||||||
|
import { useTokenStore } from '@/store/token.state';
|
||||||
|
import { useTasksStore } from '@/store/tasks.state';
|
||||||
|
import { TasksApi } from '@/api/tasks.api';
|
||||||
|
|
||||||
|
export const TasksProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const { setTasks } = useTasksStore();
|
||||||
|
const { token } = useTokenStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const api = TasksApi.getInstance(token);
|
||||||
|
api.getAllTasks().then(tasks => setTasks(tasks));
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { PropsWithChildren, useEffect } from 'react';
|
||||||
|
import { ConfigBotApi } from '@/api/config-bot.api';
|
||||||
|
import { useTokenStore } from '@/store/token.state';
|
||||||
|
|
||||||
|
const configBotApi = ConfigBotApi.getInstance();
|
||||||
|
|
||||||
|
export const TokenProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const { setToken } = useTokenStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
configBotApi.getConfigBot().then(cfg => {
|
||||||
|
console.log('[TokenProvider] got config:', cfg);
|
||||||
|
setToken(cfg.token);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[TokenProvider] error getting config:', err);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
155
CMyTapper/robucks-front/components/shop/Shop.module.css
Normal file
155
CMyTapper/robucks-front/components/shop/Shop.module.css
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
.shop {
|
||||||
|
background: #101010;
|
||||||
|
height: calc(100vh - 82px);
|
||||||
|
font-family: var(--font-manrope);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-bottom: 82px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item_img {
|
||||||
|
--size: 32px;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
max-width: var(--size);
|
||||||
|
max-height: var(--size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item_img_sm {
|
||||||
|
--size: 18px;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
max-width: var(--size);
|
||||||
|
max-height: var(--size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item_img > img, .item_img_sm > img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-top: 45px;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop.overflow {
|
||||||
|
padding-bottom: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemsAll {
|
||||||
|
color: #42BB47;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header span {
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 19px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchangeRobucks {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
min-height: 155px;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
padding: 20px 16px;
|
||||||
|
background: #0E0E10;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemsHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
cursor: pointer;
|
||||||
|
background: #FFFFFF0D;
|
||||||
|
height: 76px;
|
||||||
|
border-radius: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shopItem {
|
||||||
|
cursor: pointer;
|
||||||
|
background: #FFFFFF0D;
|
||||||
|
height: 76px;
|
||||||
|
border-radius: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shopItemPrice {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInfo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInfoTitle span {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInfoContent {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrades {
|
||||||
|
margin-top: 1rem;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
background: #0E0E10;
|
||||||
|
border: 1px solid #18181A;
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgradesTitle span {
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 19px;
|
||||||
|
line-height: 20.9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgradesContent {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgradesContent span {
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shopItems {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 0.8rem;
|
||||||
|
}
|
||||||
84
CMyTapper/robucks-front/components/shop/Shop.tsx
Normal file
84
CMyTapper/robucks-front/components/shop/Shop.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import styles from '@/components/shop/Shop.module.css';
|
||||||
|
import { FC, useEffect, useRef, useState } from 'react';
|
||||||
|
import ModalExchangeContainer from '@/components/ui/modal-exchange/ModalExchangeContainer';
|
||||||
|
import { ToastContainer } from 'react-toastify';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import ShopItem from '@/components/shop/ShopItem';
|
||||||
|
import { IHandleOpen } from '@/components/shop/ShopContainer';
|
||||||
|
import { useShopItemsStore } from '@/store/shopItems.state';
|
||||||
|
import { usePlayerStore } from '@/store/player.state';
|
||||||
|
import { toFixed } from '@/utils/FormatNumber';
|
||||||
|
|
||||||
|
interface IShop {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
handleOpen: (data: IHandleOpen) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Shop: FC<IShop> = ({ isOpen, handleOpen, handleClose }) => {
|
||||||
|
const shopBlockRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isOverflow, setIsOverflow] = useState(false);
|
||||||
|
|
||||||
|
const { shopItems } = useShopItemsStore();
|
||||||
|
const { player } = usePlayerStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkOverflow = () => {
|
||||||
|
if (shopBlockRef.current) {
|
||||||
|
setIsOverflow(
|
||||||
|
shopBlockRef.current.scrollHeight > shopBlockRef.current.clientHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkOverflow();
|
||||||
|
|
||||||
|
window.addEventListener('resize', checkOverflow);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', checkOverflow);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.shop} ${isOverflow ? styles.overflow : ''}`} ref={shopBlockRef}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<span>Магазин</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.items}>
|
||||||
|
<div className={styles.itemsHeader}>
|
||||||
|
<span>Товары</span>
|
||||||
|
<span className={styles.itemsAll}>Все</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.shopItems}>
|
||||||
|
{shopItems &&
|
||||||
|
shopItems.map(shopItem => (
|
||||||
|
<ShopItem
|
||||||
|
key={shopItem.id}
|
||||||
|
shopItem={shopItem}
|
||||||
|
handleOpen={handleOpen}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.upgrades}>
|
||||||
|
<div className={styles.upgradesTitle}>
|
||||||
|
<span>Улучшения</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.upgradesContent}>
|
||||||
|
<span>Уже скоро...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ModalExchangeContainer
|
||||||
|
isOpen={isOpen}
|
||||||
|
balance={toFixed(player?.attributes.balance)}
|
||||||
|
onClose={handleClose}
|
||||||
|
/>
|
||||||
|
<ToastContainer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Shop;
|
||||||
42
CMyTapper/robucks-front/components/shop/ShopContainer.tsx
Normal file
42
CMyTapper/robucks-front/components/shop/ShopContainer.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Shop from '@/components/shop/Shop';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useModalStore } from '@/store/modal.state';
|
||||||
|
|
||||||
|
export interface IHandleOpen {
|
||||||
|
image?: string;
|
||||||
|
isDecimal?: boolean;
|
||||||
|
priceItem?: number;
|
||||||
|
shopItem?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShopContainer = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { setShopItemId, setPriceItem, setImage, setIsDecimal } = useModalStore();
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOpen = useCallback((data: IHandleOpen) => {
|
||||||
|
if (data.image && data.priceItem && data.shopItem && data.isDecimal !== undefined) {
|
||||||
|
setIsDecimal(data.isDecimal);
|
||||||
|
setImage(data.image);
|
||||||
|
setPriceItem(data.priceItem);
|
||||||
|
setShopItemId(data.shopItem);
|
||||||
|
setIsOpen(true);
|
||||||
|
} else {
|
||||||
|
setIsOpen(true);
|
||||||
|
setIsDecimal(true);
|
||||||
|
setImage('/icons/small-coin-icon.svg');
|
||||||
|
setPriceItem(0);
|
||||||
|
setShopItemId(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <Shop isOpen={isOpen} handleClose={handleClose} handleOpen={handleOpen} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShopContainer;
|
||||||
63
CMyTapper/robucks-front/components/shop/ShopItem.tsx
Normal file
63
CMyTapper/robucks-front/components/shop/ShopItem.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { IShopItem } from '@/api/shop-item.api';
|
||||||
|
import styles from '@/components/shop/Shop.module.css';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { IHandleOpen } from '@/components/shop/ShopContainer';
|
||||||
|
|
||||||
|
interface IShopItemProps {
|
||||||
|
shopItem: IShopItem;
|
||||||
|
handleOpen: (data: IHandleOpen) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShopItem: FC<IShopItemProps> = ({ shopItem, handleOpen }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.item}
|
||||||
|
onClick={() =>
|
||||||
|
handleOpen({
|
||||||
|
image: `${process.env.NEXT_PUBLIC_STRAPI || ''}${shopItem.attributes.icon.data.attributes.url}`,
|
||||||
|
isDecimal: shopItem.attributes.isDecimal,
|
||||||
|
priceItem: shopItem.attributes.price,
|
||||||
|
shopItem: shopItem.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{shopItem?.attributes?.icon?.data && (
|
||||||
|
<div className={styles.item_img}>
|
||||||
|
<img
|
||||||
|
loading={'lazy'}
|
||||||
|
src={`${process.env.NEXT_PUBLIC_STRAPI || ''}${shopItem.attributes.icon.data.attributes.url}`}
|
||||||
|
alt={'shop item icon'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.itemInfo}>
|
||||||
|
<div className={styles.itemInfoTitle}>
|
||||||
|
<span>{shopItem.attributes.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.itemInfoContent}>
|
||||||
|
<span>{shopItem.attributes.price}</span>
|
||||||
|
<Image
|
||||||
|
src={'/icons/small-robucks-icon.svg'}
|
||||||
|
alt={'small robucks'}
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
/>
|
||||||
|
<span>=</span>
|
||||||
|
<span>1</span>
|
||||||
|
<div className={styles.item_img_sm}>
|
||||||
|
<img
|
||||||
|
loading={'lazy'}
|
||||||
|
src={`${process.env.NEXT_PUBLIC_STRAPI || ''}${shopItem.attributes.icon.data.attributes.url}`}
|
||||||
|
alt={'small coin icon'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShopItem;
|
||||||
65
CMyTapper/robucks-front/components/tasks/Task.tsx
Normal file
65
CMyTapper/robucks-front/components/tasks/Task.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ITask } from '@/api/tasks.api';
|
||||||
|
import { FC, useCallback } from 'react';
|
||||||
|
import styles from '@/components/tasks/Tasks.module.css';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { IFinishedTasks } from '@/interfaces/player.type';
|
||||||
|
import TaskContent from '@/components/tasks/TaskContent';
|
||||||
|
|
||||||
|
interface ITaskProps {
|
||||||
|
task: ITask;
|
||||||
|
finishedTask: IFinishedTasks | undefined;
|
||||||
|
handleClaimPrize: (taskId: number, prize: number) => Promise<void>;
|
||||||
|
handleOpenLink: (link: string, taskId: number) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Task: FC<ITaskProps> = ({ task, finishedTask, handleClaimPrize, handleOpenLink }) => {
|
||||||
|
const handleTaskFunction = useCallback(() => {
|
||||||
|
if (!finishedTask) {
|
||||||
|
return handleOpenLink(task.attributes.link, task.id);
|
||||||
|
}
|
||||||
|
}, [handleOpenLink, finishedTask]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
finishedTask && finishedTask.attributes.status === 'claimed'
|
||||||
|
? `${styles.taskClaimed} ${styles.task}`
|
||||||
|
: `${styles.task}`
|
||||||
|
}
|
||||||
|
onClick={handleTaskFunction}
|
||||||
|
>
|
||||||
|
{task?.attributes?.icon?.data && (
|
||||||
|
<div className={styles.taskIcon}>
|
||||||
|
<Image
|
||||||
|
src={`${process.env.NEXT_PUBLIC_STRAPI || ''}${task.attributes.icon.data.attributes.url}`}
|
||||||
|
alt={'task icon'}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.taskContent}>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
finishedTask && finishedTask.attributes.status === 'claimed'
|
||||||
|
? `${styles.taskClaimedTitle} ${styles.taskTitle}`
|
||||||
|
: `${styles.taskTitle}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{task.attributes.title}</span>
|
||||||
|
</div>
|
||||||
|
<TaskContent
|
||||||
|
taskId={task.id}
|
||||||
|
link={task.attributes.link}
|
||||||
|
status={finishedTask?.attributes.status}
|
||||||
|
prize={task.attributes.prize}
|
||||||
|
handleClaimPrize={handleClaimPrize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Task;
|
||||||
61
CMyTapper/robucks-front/components/tasks/TaskContent.tsx
Normal file
61
CMyTapper/robucks-front/components/tasks/TaskContent.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
'use client';
|
||||||
|
import { FC, useState } from 'react';
|
||||||
|
import styles from '@/components/tasks/Tasks.module.css';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
interface ITaskContent {
|
||||||
|
taskId: number;
|
||||||
|
link: string;
|
||||||
|
status: string | undefined;
|
||||||
|
prize: number;
|
||||||
|
handleClaimPrize: (taskId: number, prize: number) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskContent: FC<ITaskContent> = ({ taskId, status, prize, handleClaimPrize }) => {
|
||||||
|
const [isClaiming, setIsClaiming] = useState(false);
|
||||||
|
|
||||||
|
const onClaim = () => {
|
||||||
|
setIsClaiming(true);
|
||||||
|
handleClaimPrize(taskId, prize).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
setIsClaiming(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (status) {
|
||||||
|
if (status === 'pending_prize') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button className={styles.claimPrize} onClick={onClaim} disabled={isClaiming}>
|
||||||
|
{`Получить +${prize}`}
|
||||||
|
<Image
|
||||||
|
src={'/icons/small-robucks-icon.svg'}
|
||||||
|
alt={'small coin icon'}
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (status === 'claimed') {
|
||||||
|
return (
|
||||||
|
<div className={styles.claimedPrize}>
|
||||||
|
<span>Выполнено</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className={styles.taskPrize}>
|
||||||
|
<span>+ {prize}</span>
|
||||||
|
<Image
|
||||||
|
src={'/icons/small-robucks-icon.svg'}
|
||||||
|
alt={'small coin icon'}
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskContent;
|
||||||
109
CMyTapper/robucks-front/components/tasks/Tasks.module.css
Normal file
109
CMyTapper/robucks-front/components/tasks/Tasks.module.css
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
.tasks {
|
||||||
|
background: #101010;
|
||||||
|
padding-top: 45px;
|
||||||
|
font-family: var(--font-manrope);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasksHeader {
|
||||||
|
padding-left: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasksHeader span {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 19px;
|
||||||
|
color: white;
|
||||||
|
line-height: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasksBlock {
|
||||||
|
height: auto;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 30px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
background: #0E0E10;
|
||||||
|
border: 1px solid #18181A;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasksBlock.overflow {
|
||||||
|
padding-bottom: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #FFFFFF0D;
|
||||||
|
min-height: 75px;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
gap: 7.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskTitle span {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskPrize {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskPrize span {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #BBBBBB;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claimPrize {
|
||||||
|
font-family: var(--font-manrope);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #101010;
|
||||||
|
border: 1px solid #42BB47;
|
||||||
|
width: 267px;
|
||||||
|
height: 42px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 40px;
|
||||||
|
gap: 5px;
|
||||||
|
color: #42BB47;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claimedPrize {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claimedPrize span {
|
||||||
|
color: #42BB47;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskClaimed {
|
||||||
|
background: #42BB4740;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskClaimedTitle span {
|
||||||
|
color: #42BB47;
|
||||||
|
}
|
||||||
64
CMyTapper/robucks-front/components/tasks/Tasks.tsx
Normal file
64
CMyTapper/robucks-front/components/tasks/Tasks.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FC, useEffect, useRef, useState } from 'react';
|
||||||
|
import styles from '@/components/tasks/Tasks.module.css';
|
||||||
|
import Task from '@/components/tasks/Task';
|
||||||
|
import { useTasksStore } from '@/store/tasks.state';
|
||||||
|
import { usePlayerStore } from '@/store/player.state';
|
||||||
|
|
||||||
|
interface ITasksProps {
|
||||||
|
handleClaimPrize: (taskId: number, prize: number) => Promise<void>;
|
||||||
|
handleOpenLink: (link: string, taskId: number) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tasks: FC<ITasksProps> = ({ handleOpenLink, handleClaimPrize }) => {
|
||||||
|
const tasksBlockRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isOverflow, setIsOverflow] = useState(false);
|
||||||
|
const { tasks } = useTasksStore();
|
||||||
|
const { player } = usePlayerStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkOverflow = () => {
|
||||||
|
if (tasksBlockRef.current) {
|
||||||
|
setIsOverflow(
|
||||||
|
tasksBlockRef.current.scrollHeight > tasksBlockRef.current.clientHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkOverflow();
|
||||||
|
|
||||||
|
window.addEventListener('resize', checkOverflow);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', checkOverflow);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.tasks}>
|
||||||
|
<div className={styles.tasksHeader}>
|
||||||
|
<span>Задания</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${styles.tasksBlock} ${isOverflow ? styles.overflow : ''}`}
|
||||||
|
ref={tasksBlockRef}
|
||||||
|
>
|
||||||
|
{tasks.map(task => {
|
||||||
|
const finishedTask = player?.attributes.finished_tasks.data?.find(
|
||||||
|
el => el?.attributes.task?.data?.id === task?.id,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Task
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
finishedTask={finishedTask}
|
||||||
|
handleClaimPrize={handleClaimPrize}
|
||||||
|
handleOpenLink={handleOpenLink}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tasks;
|
||||||
54
CMyTapper/robucks-front/components/tasks/TasksContainer.tsx
Normal file
54
CMyTapper/robucks-front/components/tasks/TasksContainer.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Tasks from '@/components/tasks/Tasks';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { StatusTask } from '@/interfaces/task.type';
|
||||||
|
import { usePlayerStore } from '@/store/player.state';
|
||||||
|
import { useTokenStore } from '@/store/token.state';
|
||||||
|
import { PlayerApi } from '@/api/player.api';
|
||||||
|
|
||||||
|
const TasksContainer = () => {
|
||||||
|
const { player, setPlayer } = usePlayerStore();
|
||||||
|
const { token } = useTokenStore();
|
||||||
|
|
||||||
|
const handleClaimPrize = useCallback(
|
||||||
|
async (taskId: number) => {
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const playerApi = PlayerApi.getInstance(token);
|
||||||
|
const updatedPlayer = await playerApi.updateTasks(player!.id, {
|
||||||
|
taskId: taskId,
|
||||||
|
status: 'claimed' as StatusTask,
|
||||||
|
});
|
||||||
|
const newPlayer = await playerApi.findPlayerByAuthId(
|
||||||
|
encodeURIComponent(updatedPlayer.attributes.auth_id),
|
||||||
|
);
|
||||||
|
setPlayer(newPlayer);
|
||||||
|
},
|
||||||
|
[player, token],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpenLink = useCallback(
|
||||||
|
async (link: string, taskId: number) => {
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.Telegram.WebApp.openLink(link);
|
||||||
|
const playerApi = PlayerApi.getInstance(token);
|
||||||
|
const updatedPlayer = await playerApi.updateTasks(player!.id, {
|
||||||
|
taskId: taskId,
|
||||||
|
status: 'pending_prize' as StatusTask,
|
||||||
|
});
|
||||||
|
const newPlayer = await playerApi.findPlayerByAuthId(
|
||||||
|
encodeURIComponent(updatedPlayer.attributes.auth_id),
|
||||||
|
);
|
||||||
|
setPlayer(newPlayer);
|
||||||
|
},
|
||||||
|
[player, token],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Tasks handleClaimPrize={handleClaimPrize} handleOpenLink={handleOpenLink} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TasksContainer;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FC } from 'react';
|
||||||
|
import styles from '@/components/home/Home.module.css';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
interface IButtonMining {
|
||||||
|
isLoading: boolean;
|
||||||
|
typeButton: 'pending' | 'farming' | 'claim_reward';
|
||||||
|
remainingTime: string | null;
|
||||||
|
farm_amount: number | undefined;
|
||||||
|
handleStartFarming: () => void;
|
||||||
|
handleClaimRewards: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonMining: FC<IButtonMining> = ({
|
||||||
|
isLoading,
|
||||||
|
typeButton,
|
||||||
|
remainingTime,
|
||||||
|
farm_amount,
|
||||||
|
handleStartFarming,
|
||||||
|
handleClaimRewards,
|
||||||
|
}) => {
|
||||||
|
if (typeButton === 'pending') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
isLoading
|
||||||
|
? `${styles.disabledButton} ${styles.miningButton}`
|
||||||
|
: `${styles.miningButton}`
|
||||||
|
}
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={handleStartFarming}
|
||||||
|
>
|
||||||
|
Начать добычу
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeButton === 'farming' && remainingTime) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
isLoading
|
||||||
|
? `${styles.disabledButton} ${styles.farmingButton}`
|
||||||
|
: `${styles.farmingButton}`
|
||||||
|
}
|
||||||
|
>{`Собрать добычу через ${remainingTime}`}</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeButton === 'claim_reward') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
isLoading
|
||||||
|
? `${styles.disabledButton} ${styles.claimReward}`
|
||||||
|
: `${styles.claimReward}`
|
||||||
|
}
|
||||||
|
onClick={handleClaimRewards}
|
||||||
|
>
|
||||||
|
<div className={styles.reward}>
|
||||||
|
<span>Собрать добычу </span>
|
||||||
|
<div className={styles.claimAmount}>
|
||||||
|
<span>+</span>
|
||||||
|
<Image
|
||||||
|
src={'/icons/small-robucks-icon.svg'}
|
||||||
|
alt={'small robucks'}
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
/>
|
||||||
|
<span>{farm_amount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ButtonMining;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
right: -500px;
|
||||||
|
bottom: 90px;
|
||||||
|
height: 89px;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #000000;
|
||||||
|
border: 1px solid #143916;
|
||||||
|
transition: right 0.3s ease;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 21px;
|
||||||
|
font-family: var(--font-manrope);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeIcon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
'use client';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import styles from '@/components/ui/copy-link-toast/CopyLinkToast.module.css';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
interface ICopyLinkToast {
|
||||||
|
isVisible: boolean;
|
||||||
|
handleCloseToastLink: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CopyLinkToast: FC<ICopyLinkToast> = ({ isVisible, handleCloseToastLink }) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={`${styles.toast} ${isVisible ? styles.visible : ''}`}>
|
||||||
|
<div className={styles.closeIcon} onClick={handleCloseToastLink}>
|
||||||
|
<Image
|
||||||
|
src={'/icons/close-icon.svg'}
|
||||||
|
alt={'close icon'}
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.img}>
|
||||||
|
<Image
|
||||||
|
src={'/icons/info-icon.svg'}
|
||||||
|
alt={'info icon'}
|
||||||
|
height={24}
|
||||||
|
width={24}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.info}>
|
||||||
|
<p>Приглашение скопировано 🔗</p>
|
||||||
|
<span>Отправьте эту ссылку своим друзьям 📨</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CopyLinkToast;
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.isOpen {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContent {
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
background: #1A1A1C;
|
||||||
|
padding: 24px 16px 0 16px;
|
||||||
|
width: 100%;
|
||||||
|
height: 85%;
|
||||||
|
position: fixed;
|
||||||
|
bottom: -85%;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContent.isOpen {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContent.isClosing {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton::before,
|
||||||
|
.closeButton::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 18px;
|
||||||
|
height: 2px;
|
||||||
|
background: #FFFFFF26;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton::before {
|
||||||
|
transform: translate(-50%, -50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton::after {
|
||||||
|
transform: translate(-50%, -50%) rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchangeRate {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton {
|
||||||
|
background-color: green;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalTitle {
|
||||||
|
margin-top: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 19px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectAmount {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectAmountTitle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRange {
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRange input[type='range'] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 5px;
|
||||||
|
background: #333;
|
||||||
|
border-radius: 5px;
|
||||||
|
outline: none;
|
||||||
|
transition: opacity .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRange input[type='range']::-moz-range-progress {
|
||||||
|
background-color: #17da17;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRange input[type='range']::-moz-range-track {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRange input[type='range']::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #42BB47;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRange input[type='range']::-moz-range-thumb {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #42BB47;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
background: #FFFFFF0D;
|
||||||
|
padding: 16px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate {
|
||||||
|
background: #FFFFFF0D;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate span {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #BBBBBB;
|
||||||
|
line-height: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname input {
|
||||||
|
background: #FFFFFF0D;
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname input::placeholder {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #BBBBBB;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nicknameTitle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedAmount {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton {
|
||||||
|
width: 100%;
|
||||||
|
background: #42BB47;
|
||||||
|
color: white;
|
||||||
|
border-radius: 30px;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 21px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
'use client';
|
||||||
|
import { Dispatch, FC, RefObject, SetStateAction } from 'react';
|
||||||
|
import styles from '@/components/ui/modal-exchange/ModalExchange.module.css';
|
||||||
|
import ModalExchangeContent from '@/components/ui/modal-exchange/ModalExchangeContent';
|
||||||
|
import { useModalStore } from '@/store/modal.state';
|
||||||
|
|
||||||
|
interface IModalExchange {
|
||||||
|
isOpen: boolean;
|
||||||
|
isClosing: boolean;
|
||||||
|
amount: number;
|
||||||
|
balance: number | undefined;
|
||||||
|
nickname: string;
|
||||||
|
rangeRef: RefObject<HTMLInputElement>;
|
||||||
|
setNickname: Dispatch<SetStateAction<string>>;
|
||||||
|
setAmount: Dispatch<SetStateAction<number>>;
|
||||||
|
onClose: () => void;
|
||||||
|
handleCreateExchange: (amount: number, total: number, nickname: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalExchange: FC<IModalExchange> = ({
|
||||||
|
isOpen,
|
||||||
|
isClosing,
|
||||||
|
amount,
|
||||||
|
balance,
|
||||||
|
nickname,
|
||||||
|
rangeRef,
|
||||||
|
onClose,
|
||||||
|
setNickname,
|
||||||
|
setAmount,
|
||||||
|
handleCreateExchange,
|
||||||
|
}) => {
|
||||||
|
const { priceItem, image, isDecimal } = useModalStore();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.modal} ${isOpen ? styles.isOpen : ''} ${isClosing ? styles.isClosing : ''}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`${styles.modalContent} ${isOpen ? styles.isOpen : ''} ${isClosing ? styles.isClosing : ''}`}
|
||||||
|
>
|
||||||
|
<div className={styles.closeButton} onClick={onClose} />
|
||||||
|
<div className={styles.modalTitle}>
|
||||||
|
<span>Обмен</span>
|
||||||
|
</div>
|
||||||
|
<ModalExchangeContent
|
||||||
|
isDecimal={isDecimal}
|
||||||
|
balance={balance}
|
||||||
|
nickname={nickname}
|
||||||
|
amount={amount}
|
||||||
|
image={image}
|
||||||
|
priceItem={priceItem}
|
||||||
|
rangeRef={rangeRef}
|
||||||
|
setNickname={setNickname}
|
||||||
|
setAmount={setAmount}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={styles.submitButton}
|
||||||
|
onClick={() => handleCreateExchange(amount, amount / +priceItem, nickname)}
|
||||||
|
>
|
||||||
|
Вывод
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalExchange;
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import ModalExchange from '@/components/ui/modal-exchange/ModalExchange';
|
||||||
|
import { usePlayerStore } from '@/store/player.state';
|
||||||
|
import { useConfigStore } from '@/store/config.state';
|
||||||
|
import { useNotify } from '@/hooks/useNotify';
|
||||||
|
import { ExchangeRequestApi } from '@/api/exchange-request.api';
|
||||||
|
import { useModalStore } from '@/store/modal.state';
|
||||||
|
import { useTokenStore } from '@/store/token.state';
|
||||||
|
import { toFixed } from '@/utils/FormatNumber';
|
||||||
|
|
||||||
|
interface IModalExchangeContainer {
|
||||||
|
isOpen: boolean;
|
||||||
|
balance: number | undefined;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalExchangeContainer: FC<IModalExchangeContainer> = ({ balance, isOpen, onClose }) => {
|
||||||
|
const { token } = useTokenStore();
|
||||||
|
|
||||||
|
const [amount, setAmount] = useState<number>(1);
|
||||||
|
const [nickname, setNickname] = useState<string>('');
|
||||||
|
const [isClosing, setIsClosing] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const rangeRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const { player, setPlayer } = usePlayerStore();
|
||||||
|
const { config } = useConfigStore();
|
||||||
|
const { shopItemId, isDecimal, image, setShopItemId, setIsDecimal } = useModalStore();
|
||||||
|
|
||||||
|
const { getSuccessNotify, getErrorNotifyByCount, getErrorNotifyByNickname } = useNotify();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (rangeRef.current) {
|
||||||
|
rangeRef.current.style.setProperty('--value', amount.toString());
|
||||||
|
}
|
||||||
|
}, [amount]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setIsClosing(false);
|
||||||
|
setAmount(0);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsClosing(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
onClose();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateExchange = useCallback(
|
||||||
|
async (amount: number, total: number, nickname: string) => {
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const exchangeRequestApi = ExchangeRequestApi.getInstance(token);
|
||||||
|
if (player && config) {
|
||||||
|
if (nickname.length === 0) {
|
||||||
|
getErrorNotifyByNickname();
|
||||||
|
} else if (total === 0) {
|
||||||
|
getErrorNotifyByCount();
|
||||||
|
} else {
|
||||||
|
exchangeRequestApi
|
||||||
|
.createExchange({
|
||||||
|
amount: amount,
|
||||||
|
total: total,
|
||||||
|
playerId: player.id,
|
||||||
|
nickname: nickname,
|
||||||
|
balance: player.attributes.balance,
|
||||||
|
telegram_id: config.attributes.admins.data.map(
|
||||||
|
user => user.attributes.telegram_id,
|
||||||
|
),
|
||||||
|
shop_item: shopItemId || undefined,
|
||||||
|
archive_request_id: player!.attributes.archive_request!.data?.id || -1,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (player) {
|
||||||
|
setPlayer({
|
||||||
|
...player,
|
||||||
|
attributes: {
|
||||||
|
...player.attributes,
|
||||||
|
balance: toFixed(player?.attributes.balance) - amount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setShopItemId(null);
|
||||||
|
setIsDecimal(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
getSuccessNotify();
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[player, token, shopItemId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalExchange
|
||||||
|
isOpen={isOpen}
|
||||||
|
isClosing={isClosing}
|
||||||
|
balance={balance}
|
||||||
|
amount={amount}
|
||||||
|
nickname={nickname}
|
||||||
|
rangeRef={rangeRef}
|
||||||
|
onClose={handleClose}
|
||||||
|
setNickname={setNickname}
|
||||||
|
setAmount={setAmount}
|
||||||
|
handleCreateExchange={handleCreateExchange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalExchangeContainer;
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Dispatch, FC, RefObject, SetStateAction } from 'react';
|
||||||
|
import styles from '@/components/ui/modal-exchange/ModalExchange.module.css';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import NicknameInput from '@/components/ui/modal-exchange/inputs/NicknameInput';
|
||||||
|
import RangeInput from '@/components/ui/modal-exchange/inputs/RangeInput';
|
||||||
|
import { toFixed } from '@/utils/FormatNumber';
|
||||||
|
|
||||||
|
interface IModalExchangeContent {
|
||||||
|
isDecimal: boolean;
|
||||||
|
balance: number | undefined;
|
||||||
|
nickname: string;
|
||||||
|
amount: number;
|
||||||
|
priceItem: number;
|
||||||
|
image: string;
|
||||||
|
rangeRef: RefObject<HTMLInputElement>;
|
||||||
|
setNickname: Dispatch<SetStateAction<string>>;
|
||||||
|
setAmount: Dispatch<SetStateAction<number>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalExchangeContent: FC<IModalExchangeContent> = ({
|
||||||
|
isDecimal,
|
||||||
|
balance,
|
||||||
|
nickname,
|
||||||
|
amount,
|
||||||
|
priceItem,
|
||||||
|
image,
|
||||||
|
rangeRef,
|
||||||
|
setNickname,
|
||||||
|
setAmount,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.selectAmount}>
|
||||||
|
<span className={styles.selectAmountTitle}>Выберите количество</span>
|
||||||
|
<div className={styles.amount}>
|
||||||
|
<div className={styles.selectedAmount}>
|
||||||
|
<span>{`${amount.toFixed(isDecimal ? 3 : 0)} - ${toFixed(balance)}`}</span>
|
||||||
|
<Image
|
||||||
|
src={'/icons/small-robucks-icon.svg'}
|
||||||
|
alt={'small robucks icon'}
|
||||||
|
height={18}
|
||||||
|
width={18}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<RangeInput
|
||||||
|
isDecimal={isDecimal}
|
||||||
|
balance={balance}
|
||||||
|
amount={amount}
|
||||||
|
priceItem={priceItem}
|
||||||
|
rangeRef={rangeRef}
|
||||||
|
setAmount={setAmount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Image
|
||||||
|
src={'/icons/exchange-icon.svg'}
|
||||||
|
alt={'exchange icon'}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rate}>
|
||||||
|
{priceItem && (
|
||||||
|
<span>{isDecimal ? (amount / priceItem).toFixed(3) : amount / priceItem}</span>
|
||||||
|
)}
|
||||||
|
<Image src={image} alt={'small coin icon'} height={24} width={24} />
|
||||||
|
</div>
|
||||||
|
<NicknameInput nickname={nickname} setNickname={setNickname} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalExchangeContent;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
import React, { Dispatch, FC, SetStateAction, useState } from 'react';
|
||||||
|
import styles from '@/components/ui/modal-exchange/ModalExchange.module.css';
|
||||||
|
|
||||||
|
interface INicknameInput {
|
||||||
|
nickname: string;
|
||||||
|
setNickname: Dispatch<SetStateAction<string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NicknameInput: FC<INicknameInput> = ({ nickname, setNickname }) => {
|
||||||
|
const handleChangeNickname = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setNickname(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.nickname}>
|
||||||
|
<span className={styles.nicknameTitle}>Укажите ваш профиль в Roblox</span>
|
||||||
|
<input
|
||||||
|
placeholder={'Ник в Roblox'}
|
||||||
|
type={'text'}
|
||||||
|
value={nickname}
|
||||||
|
onChange={handleChangeNickname}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NicknameInput;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
'use client';
|
||||||
|
import { Dispatch, FC, SetStateAction } from 'react';
|
||||||
|
import styles from '@/components/ui/modal-exchange/ModalExchange.module.css';
|
||||||
|
|
||||||
|
interface IRangeInput {
|
||||||
|
isDecimal: boolean;
|
||||||
|
balance: number | undefined;
|
||||||
|
amount: number;
|
||||||
|
priceItem: number;
|
||||||
|
rangeRef: React.RefObject<HTMLInputElement>;
|
||||||
|
setAmount: Dispatch<SetStateAction<number>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RangeInput: FC<IRangeInput> = ({
|
||||||
|
isDecimal,
|
||||||
|
balance,
|
||||||
|
priceItem,
|
||||||
|
amount,
|
||||||
|
rangeRef,
|
||||||
|
setAmount,
|
||||||
|
}) => {
|
||||||
|
const handleRangeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
let value = parseFloat(e.target.value);
|
||||||
|
if (!isDecimal) {
|
||||||
|
value = Math.round(value / priceItem) * priceItem;
|
||||||
|
} else {
|
||||||
|
value = parseFloat(value.toFixed(6));
|
||||||
|
}
|
||||||
|
setAmount(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.inputRange}>
|
||||||
|
<input
|
||||||
|
type='range'
|
||||||
|
id='amountRange'
|
||||||
|
min='0'
|
||||||
|
max={balance}
|
||||||
|
step={isDecimal ? '0.000001' : priceItem.toString()}
|
||||||
|
value={amount}
|
||||||
|
onChange={handleRangeChange}
|
||||||
|
ref={rangeRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RangeInput;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
position: fixed;
|
||||||
|
height: 82px;
|
||||||
|
width: 100%;
|
||||||
|
background: #101010;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav_menu, .active_nav_menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 16.3px;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countTasks {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
right: 10%;
|
||||||
|
top: -10%;
|
||||||
|
background: #D92000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active_nav_menu span, .active_nav_menu img {
|
||||||
|
color: #42BB47;
|
||||||
|
}
|
||||||
126
CMyTapper/robucks-front/components/ui/navigation/Navigation.tsx
Normal file
126
CMyTapper/robucks-front/components/ui/navigation/Navigation.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
'use client';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import styles from './Navigation.module.css';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { usePlayerStore } from '@/store/player.state';
|
||||||
|
import { useTasksStore } from '@/store/tasks.state';
|
||||||
|
import { ProvidersProps } from '@/components/providers/Providers';
|
||||||
|
|
||||||
|
const Navigation = ({ id }: Pick<ProvidersProps, 'id'>) => {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const { player } = usePlayerStore();
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
|
const { tasks } = useTasksStore();
|
||||||
|
const countTasks = tasks.length - (player?.attributes.finished_tasks.data?.length || 0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [isLoading]);
|
||||||
|
|
||||||
|
const getNavLink = (endpoint: string) => {
|
||||||
|
return `/${id}/${endpoint}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Link href={getNavLink('home')} style={{ textDecoration: 'none' }}>
|
||||||
|
<div className={pathname === '/' ? styles.active_nav_menu : styles.nav_menu}>
|
||||||
|
{pathname === '/' ? (
|
||||||
|
<Image
|
||||||
|
src={'/icons/pickaxe-icon-selected.svg'}
|
||||||
|
alt={'pickaxe-icon'}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src={'/icons/pickaxe-icon.svg'}
|
||||||
|
alt={'pickaxe-icon'}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>Главная</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<Link href={getNavLink('shop')} style={{ textDecoration: 'none' }}>
|
||||||
|
<div className={pathname === '/shop' ? styles.active_nav_menu : styles.nav_menu}>
|
||||||
|
{pathname === '/shop' ? (
|
||||||
|
<Image
|
||||||
|
src={'/icons/shop-icon-selected.svg'}
|
||||||
|
alt={'shop-icon'}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src={'/icons/shop-icon.svg'}
|
||||||
|
alt={'shop-icon'}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>Магазин</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={getNavLink('tasks')}
|
||||||
|
style={{ textDecoration: 'none', position: 'relative', height: '82px' }}
|
||||||
|
>
|
||||||
|
{countTasks !== 0 && <div className={styles.countTasks}>{countTasks}</div>}
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
pathname === '/tasks'
|
||||||
|
? `${styles.active_nav_menu} ${styles.task}`
|
||||||
|
: `${styles.nav_menu} ${styles.task}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{pathname === '/tasks' ? (
|
||||||
|
<Image
|
||||||
|
src={'/icons/task-list-icon-selected.svg'}
|
||||||
|
alt={'task-list-icon'}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src={'/icons/task-list-icon.svg'}
|
||||||
|
alt={'task-list-icon'}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>Задания</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<Link href={getNavLink('friends')} style={{ textDecoration: 'none' }}>
|
||||||
|
<div className={pathname === '/friends' ? styles.active_nav_menu : styles.nav_menu}>
|
||||||
|
{pathname === '/friends' ? (
|
||||||
|
<Image
|
||||||
|
src={'/icons/friends-icon-selected.svg'}
|
||||||
|
alt={'friends-icon'}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src={'/icons/friends-icon.svg'}
|
||||||
|
alt={'friends-icon'}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>Друзья</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navigation;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.wrapper {
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
12
CMyTapper/robucks-front/components/ui/wrapper/Wrapper.tsx
Normal file
12
CMyTapper/robucks-front/components/ui/wrapper/Wrapper.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import {PropsWithChildren} from "react";
|
||||||
|
import styles from './Wrapper.module.css'
|
||||||
|
|
||||||
|
const Wrapper = ({ children } : PropsWithChildren) => {
|
||||||
|
return(
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Wrapper;
|
||||||
10
CMyTapper/robucks-front/ecosystem.config.js
Normal file
10
CMyTapper/robucks-front/ecosystem.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'webapp',
|
||||||
|
script: 'npm run build && npm run start',
|
||||||
|
time: true,
|
||||||
|
log_date_format: 'DD.MM.YYYY HH:mm:ss',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
53
CMyTapper/robucks-front/hooks/useNotify.tsx
Normal file
53
CMyTapper/robucks-front/hooks/useNotify.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
interface IUseNotifyReturn {
|
||||||
|
getSuccessNotify: () => number | string;
|
||||||
|
getErrorNotifyByNickname: () => number | string;
|
||||||
|
getErrorNotifyByCount: () => number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNotify(): IUseNotifyReturn {
|
||||||
|
const getSuccessNotify = () => {
|
||||||
|
return toast.success('Заявка успешно создана!', {
|
||||||
|
position: 'top-left',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
style: { width: '250px', color: 'black', margin: '15px' },
|
||||||
|
progress: undefined,
|
||||||
|
theme: 'dark',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getErrorNotifyByNickname = () => {
|
||||||
|
return toast.error('Вы не указали ваш никнейм!', {
|
||||||
|
position: 'top-left',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
style: { width: '250px', color: 'black', margin: '15px' },
|
||||||
|
progress: undefined,
|
||||||
|
theme: 'dark',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getErrorNotifyByCount = () => {
|
||||||
|
return toast.error('Вы не можете выбрать количество равное 0!', {
|
||||||
|
position: 'top-left',
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
style: { width: '250px', color: 'black', margin: '15px' },
|
||||||
|
progress: undefined,
|
||||||
|
theme: 'dark',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { getSuccessNotify, getErrorNotifyByNickname, getErrorNotifyByCount };
|
||||||
|
}
|
||||||
7
CMyTapper/robucks-front/interfaces/global.d.ts
vendored
Normal file
7
CMyTapper/robucks-front/interfaces/global.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export {};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
Telegram: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
CMyTapper/robucks-front/interfaces/player.type.ts
Normal file
40
CMyTapper/robucks-front/interfaces/player.type.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { StatusTask } from '@/interfaces/task.type';
|
||||||
|
import { ITask } from '@/api/tasks.api';
|
||||||
|
import { IArchiveRequest } from '@/api/archive-request.api';
|
||||||
|
|
||||||
|
export interface IPlayer {
|
||||||
|
id: number;
|
||||||
|
attributes: {
|
||||||
|
telegram_nick: string;
|
||||||
|
telegram_id: number;
|
||||||
|
auth_id: string;
|
||||||
|
balance: number;
|
||||||
|
old_balance: number;
|
||||||
|
referral_fee: number;
|
||||||
|
my_referrals: { data: IPlayer[] | null };
|
||||||
|
referral: { data: IPlayer | null };
|
||||||
|
start_farm: Date | null;
|
||||||
|
finished_tasks: { data: IFinishedTasks[] | null };
|
||||||
|
archive_request: { data: IArchiveRequest | null };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFinishedTasks {
|
||||||
|
id: number;
|
||||||
|
attributes: {
|
||||||
|
status: StatusTask;
|
||||||
|
task: { data: ITask | null };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICreatePlayer {
|
||||||
|
telegram_id: number;
|
||||||
|
telegram_nick: string;
|
||||||
|
balance: number;
|
||||||
|
old_balance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUpdatePlayerTask {
|
||||||
|
status: StatusTask;
|
||||||
|
taskId: number;
|
||||||
|
}
|
||||||
4
CMyTapper/robucks-front/interfaces/task.type.ts
Normal file
4
CMyTapper/robucks-front/interfaces/task.type.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum StatusTask {
|
||||||
|
'pending_prize' = 'pending_prize',
|
||||||
|
'claimed' = 'claimed',
|
||||||
|
}
|
||||||
28
CMyTapper/robucks-front/next.config.mjs
Normal file
28
CMyTapper/robucks-front/next.config.mjs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
hostname: 'strapi.rbx-click.ru'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'http',
|
||||||
|
hostname: '127.0.0.1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'http',
|
||||||
|
hostname: 'localhost'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/uploads/:path*',
|
||||||
|
destination: 'http://127.0.0.1:1337/uploads/:path*',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
5300
CMyTapper/robucks-front/package-lock.json
generated
Normal file
5300
CMyTapper/robucks-front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
CMyTapper/robucks-front/package.json
Normal file
42
CMyTapper/robucks-front/package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "clicker-front",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@next/font": "^14.2.4",
|
||||||
|
"@types/axios": "^0.14.0",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"@types/react-toastify": "^4.1.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
|
"axios": "^1.7.2",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"eslint-plugin-unused-imports": "^4.0.0",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"next": "14.2.4",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"react-toastify": "^10.0.5",
|
||||||
|
"zustand": "^4.5.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"eslint": "8",
|
||||||
|
"eslint-config-next": "14.2.4",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
CMyTapper/robucks-front/public/fonts/ClashDisplay-Bold.otf
Normal file
BIN
CMyTapper/robucks-front/public/fonts/ClashDisplay-Bold.otf
Normal file
Binary file not shown.
BIN
CMyTapper/robucks-front/public/fonts/ClashDisplay-Extralight.otf
Normal file
BIN
CMyTapper/robucks-front/public/fonts/ClashDisplay-Extralight.otf
Normal file
Binary file not shown.
BIN
CMyTapper/robucks-front/public/fonts/ClashDisplay-Light.otf
Normal file
BIN
CMyTapper/robucks-front/public/fonts/ClashDisplay-Light.otf
Normal file
Binary file not shown.
BIN
CMyTapper/robucks-front/public/fonts/ClashDisplay-Medium.otf
Normal file
BIN
CMyTapper/robucks-front/public/fonts/ClashDisplay-Medium.otf
Normal file
Binary file not shown.
BIN
CMyTapper/robucks-front/public/fonts/ClashDisplay-Regular.otf
Normal file
BIN
CMyTapper/robucks-front/public/fonts/ClashDisplay-Regular.otf
Normal file
Binary file not shown.
BIN
CMyTapper/robucks-front/public/fonts/ClashDisplay-Semibold.otf
Normal file
BIN
CMyTapper/robucks-front/public/fonts/ClashDisplay-Semibold.otf
Normal file
Binary file not shown.
15
CMyTapper/robucks-front/public/globals.css
Normal file
15
CMyTapper/robucks-front/public/globals.css
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
:root {
|
||||||
|
--font-manrope: var(--font-manrope);
|
||||||
|
--font-clash: var(--font-clash);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
11
CMyTapper/robucks-front/public/icons/close-icon.svg
Normal file
11
CMyTapper/robucks-front/public/icons/close-icon.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_81_2696)">
|
||||||
|
<path d="M1 1L11 11" stroke="white" stroke-linecap="round"/>
|
||||||
|
<path d="M1 11L11 1" stroke="white" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_81_2696">
|
||||||
|
<rect width="12" height="12" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 367 B |
6
CMyTapper/robucks-front/public/icons/exchange-icon.svg
Normal file
6
CMyTapper/robucks-front/public/icons/exchange-icon.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.5 8L7.5 5L4.5 8" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M7.5 19V5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M14.5 16L17.5 19L20.5 16" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M17.5 5V19" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 541 B |
5
CMyTapper/robucks-front/public/icons/friend-icon.svg
Normal file
5
CMyTapper/robucks-front/public/icons/friend-icon.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M32.1369 9.86319C38.2877 16.014 38.2877 25.9863 32.1369 32.137C25.9861 38.2878 16.0138 38.2878 9.86307 32.137C3.71231 25.9863 3.71231 16.0139 9.86307 9.86319C16.0138 3.71243 25.9862 3.71243 32.1369 9.86319" stroke="white" stroke-width="2.625" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M24.4806 14.567C26.403 16.4894 26.403 19.6063 24.4806 21.5288C22.5581 23.4512 19.4412 23.4512 17.5187 21.5288C15.5963 19.6063 15.5963 16.4894 17.5187 14.567C19.4412 12.6445 22.5581 12.6445 24.4806 14.567" stroke="white" stroke-width="2.625" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M30.9872 33.1766C28.4759 30.5324 24.9339 28.8751 20.9999 28.8751C17.0659 28.8751 13.5239 30.5324 11.0127 33.1784" stroke="white" stroke-width="2.625" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 911 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.5 17C8.88071 17 10 15.8807 10 14.5C10 13.1193 8.88071 12 7.5 12C6.11929 12 5 13.1193 5 14.5C5 15.8807 6.11929 17 7.5 17Z" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M17.5 17C18.8807 17 20 15.8807 20 14.5C20 13.1193 18.8807 12 17.5 12C16.1193 12 15 13.1193 15 14.5C15 15.8807 16.1193 17 17.5 17Z" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M12.5 7C13.8807 7 15 5.88071 15 4.5C15 3.11929 13.8807 2 12.5 2C11.1193 2 10 3.11929 10 4.5C10 5.88071 11.1193 7 12.5 7Z" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M12.5 22C12.5 19.2386 10.2614 17 7.5 17C4.73857 17 2.5 19.2386 2.5 22" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M22.5 22C22.5 19.2386 20.2614 17 17.5 17C14.7386 17 12.5 19.2386 12.5 22" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<path d="M17.5 12C17.5 9.2386 15.2614 7 12.5 7C9.7386 7 7.5 9.2386 7.5 12" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user