Update files
This commit is contained in:
parent
e9afda5e16
commit
af063ecc7d
|
|
@ -0,0 +1,199 @@
|
||||||
|
const axios = require('axios');
|
||||||
|
const config = require('../config');
|
||||||
|
const { log, logError } = require('../middleware/logger');
|
||||||
|
const User = require('../models/User');
|
||||||
|
|
||||||
|
const BOT_TOKEN = config.telegramBotToken;
|
||||||
|
const TELEGRAM_API = BOT_TOKEN ? `https://api.telegram.org/bot${BOT_TOKEN}` : null;
|
||||||
|
|
||||||
|
let isPolling = false;
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
const sendMessage = async (chatId, text, options = {}) => {
|
||||||
|
if (!TELEGRAM_API) {
|
||||||
|
log('warn', 'TELEGRAM_BOT_TOKEN не установлен, отправка сообщения невозможна');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${TELEGRAM_API}/sendMessage`, {
|
||||||
|
chat_id: chatId,
|
||||||
|
text,
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
logError('Ошибка отправки сообщения', error, { chatId, text: text.substring(0, 50) });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessageToAllUsers = async (messageText) => {
|
||||||
|
if (!TELEGRAM_API) {
|
||||||
|
throw new Error('TELEGRAM_BOT_TOKEN не установлен');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const users = await User.find({ banned: { $ne: true } }).select('telegramId');
|
||||||
|
let sent = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
try {
|
||||||
|
await sendMessage(user.telegramId, messageText);
|
||||||
|
sent++;
|
||||||
|
// Небольшая задержка, чтобы не превысить лимиты API
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
} catch (error) {
|
||||||
|
failed++;
|
||||||
|
logError('Ошибка отправки сообщения пользователю', error, { telegramId: user.telegramId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sent, failed, total: users.length };
|
||||||
|
} catch (error) {
|
||||||
|
logError('Ошибка массовой отправки сообщений', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStartMessage = () => {
|
||||||
|
return `👋 <b>Добро пожаловать в Nakama!</b>
|
||||||
|
|
||||||
|
📱 <b>Nakama</b> — социальная сеть для фурри и аниме сообщества.
|
||||||
|
|
||||||
|
<b>Основные возможности:</b>
|
||||||
|
• Создание постов с текстом и изображениями
|
||||||
|
• Поиск контента через e621 и Gelbooru
|
||||||
|
• Комментарии и лайки
|
||||||
|
• Подписки на пользователей
|
||||||
|
• Система уведомлений
|
||||||
|
• Фильтры и теги
|
||||||
|
|
||||||
|
<b>Как начать:</b>
|
||||||
|
1. Нажмите кнопку "Войти" ниже, чтобы запустить приложение
|
||||||
|
2. Создайте свой первый пост
|
||||||
|
3. Подписывайтесь на интересных пользователей
|
||||||
|
|
||||||
|
<b>Поддержка:</b>
|
||||||
|
Если возникли проблемы, напишите @NakamaReportbot
|
||||||
|
|
||||||
|
Приятного использования!`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCommand = async (message) => {
|
||||||
|
const chatId = message.chat.id;
|
||||||
|
const text = (message.text || '').trim();
|
||||||
|
const args = text.split(/\s+/);
|
||||||
|
const command = args[0].toLowerCase();
|
||||||
|
|
||||||
|
if (command === '/start') {
|
||||||
|
const startParam = message.text.split(' ')[1] || '';
|
||||||
|
|
||||||
|
// Если есть start_param (например, post_12345 или ref_ABC123)
|
||||||
|
// Это обрабатывается при открытии миниаппа, здесь просто показываем инструкцию
|
||||||
|
const startMessage = getStartMessage();
|
||||||
|
|
||||||
|
// Добавить кнопку для открытия миниаппа
|
||||||
|
let botUsername = 'NakamaSpaceBot';
|
||||||
|
if (config.telegramBotToken) {
|
||||||
|
try {
|
||||||
|
const botInfo = await axios.get(`${TELEGRAM_API}/getMe`);
|
||||||
|
botUsername = botInfo.data.result.username || 'NakamaSpaceBot';
|
||||||
|
} catch (error) {
|
||||||
|
log('warn', 'Не удалось получить имя бота', { error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendMessage(chatId, startMessage, {
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [[
|
||||||
|
{
|
||||||
|
text: '🚀 Открыть Nakama',
|
||||||
|
web_app: {
|
||||||
|
url: `https://t.me/${botUsername}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Игнорируем неизвестные команды
|
||||||
|
};
|
||||||
|
|
||||||
|
const processUpdate = async (update) => {
|
||||||
|
const message = update.message || update.edited_message;
|
||||||
|
if (!message || !message.text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handleCommand(message);
|
||||||
|
} catch (error) {
|
||||||
|
logError('Ошибка обработки команды основного бота', error, {
|
||||||
|
chatId: message.chat.id,
|
||||||
|
text: message.text?.substring(0, 50)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pollUpdates = async () => {
|
||||||
|
if (!TELEGRAM_API) {
|
||||||
|
log('warn', 'Основной бот не запущен: TELEGRAM_BOT_TOKEN не установлен');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPolling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPolling = true;
|
||||||
|
log('info', 'Основной бот запущен, опрос обновлений...');
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${TELEGRAM_API}/getUpdates`, {
|
||||||
|
params: {
|
||||||
|
offset,
|
||||||
|
timeout: 30,
|
||||||
|
allowed_updates: ['message']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updates = response.data.result || [];
|
||||||
|
|
||||||
|
for (const update of updates) {
|
||||||
|
offset = update.update_id + 1;
|
||||||
|
await processUpdate(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Продолжить опрос
|
||||||
|
setTimeout(poll, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
logError('Ошибка опроса Telegram для основного бота', error);
|
||||||
|
// Переподключиться через 5 секунд
|
||||||
|
setTimeout(poll, 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
poll();
|
||||||
|
};
|
||||||
|
|
||||||
|
const startMainBot = () => {
|
||||||
|
if (!BOT_TOKEN) {
|
||||||
|
log('warn', 'Основной бот не запущен: TELEGRAM_BOT_TOKEN не установлен');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pollUpdates();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
startMainBot,
|
||||||
|
sendMessageToAllUsers,
|
||||||
|
sendMessage
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -159,6 +159,7 @@ const authenticate = async (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const telegramUser = payload.user;
|
const telegramUser = payload.user;
|
||||||
|
const startParam = payload.start_param || payload.startParam;
|
||||||
|
|
||||||
if (!validateTelegramId(telegramUser.id)) {
|
if (!validateTelegramId(telegramUser.id)) {
|
||||||
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id });
|
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id });
|
||||||
|
|
@ -171,14 +172,28 @@ const authenticate = async (req, res, next) => {
|
||||||
let user = await User.findOne({ telegramId: normalizedUser.id.toString() });
|
let user = await User.findOne({ telegramId: normalizedUser.id.toString() });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
// Обработка реферального кода из start_param
|
||||||
|
let referredBy = null;
|
||||||
|
if (startParam && startParam.startsWith('ref_')) {
|
||||||
|
const referralCode = startParam;
|
||||||
|
const referrer = await User.findOne({ referralCode });
|
||||||
|
if (referrer) {
|
||||||
|
referredBy = referrer._id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
user = new User({
|
user = new User({
|
||||||
telegramId: normalizedUser.id.toString(),
|
telegramId: normalizedUser.id.toString(),
|
||||||
username: normalizedUser.username || normalizedUser.firstName || 'user',
|
username: normalizedUser.username || normalizedUser.firstName || 'user',
|
||||||
firstName: normalizedUser.firstName,
|
firstName: normalizedUser.firstName,
|
||||||
lastName: normalizedUser.lastName,
|
lastName: normalizedUser.lastName,
|
||||||
photoUrl: normalizedUser.photoUrl
|
photoUrl: normalizedUser.photoUrl,
|
||||||
|
referredBy: referredBy
|
||||||
});
|
});
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
|
// Счетчик рефералов увеличивается только когда пользователь создаст первый пост
|
||||||
|
// (см. routes/posts.js)
|
||||||
} else {
|
} else {
|
||||||
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
|
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
|
||||||
if (normalizedUser.username) {
|
if (normalizedUser.username) {
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,35 @@ const UserSchema = new mongoose.Schema({
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
bannedUntil: Date,
|
bannedUntil: Date,
|
||||||
|
// Реферальная система
|
||||||
|
referralCode: {
|
||||||
|
type: String,
|
||||||
|
unique: true,
|
||||||
|
sparse: true
|
||||||
|
},
|
||||||
|
referredBy: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'User'
|
||||||
|
},
|
||||||
|
referralsCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: Date.now
|
default: Date.now
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Генерировать реферальный код перед сохранением
|
||||||
|
UserSchema.pre('save', async function(next) {
|
||||||
|
if (!this.referralCode) {
|
||||||
|
// Генерировать уникальный код на основе telegramId
|
||||||
|
const code = `ref_${this.telegramId.slice(-8)}${Math.random().toString(36).substring(2, 6)}`.toUpperCase();
|
||||||
|
this.referralCode = code;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = mongoose.model('User', UserSchema);
|
module.exports = mongoose.model('User', UserSchema);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ const respondWithUser = async (user, res) => {
|
||||||
role: populatedUser.role,
|
role: populatedUser.role,
|
||||||
followersCount: populatedUser.followers.length,
|
followersCount: populatedUser.followers.length,
|
||||||
followingCount: populatedUser.following.length,
|
followingCount: populatedUser.following.length,
|
||||||
|
referralCode: populatedUser.referralCode,
|
||||||
|
referralsCount: populatedUser.referralsCount || 0,
|
||||||
settings,
|
settings,
|
||||||
banned: populatedUser.banned
|
banned: populatedUser.banned
|
||||||
}
|
}
|
||||||
|
|
@ -232,6 +234,8 @@ router.post('/oauth', strictAuthLimiter, async (req, res) => {
|
||||||
role: populatedUser.role,
|
role: populatedUser.role,
|
||||||
followersCount: populatedUser.followers.length,
|
followersCount: populatedUser.followers.length,
|
||||||
followingCount: populatedUser.following.length,
|
followingCount: populatedUser.following.length,
|
||||||
|
referralCode: populatedUser.referralCode,
|
||||||
|
referralsCount: populatedUser.referralsCount || 0,
|
||||||
settings,
|
settings,
|
||||||
banned: populatedUser.banned
|
banned: populatedUser.banned
|
||||||
}
|
}
|
||||||
|
|
@ -277,6 +281,8 @@ router.post('/session', async (req, res) => {
|
||||||
{ path: 'following', select: 'username firstName lastName photoUrl' }
|
{ path: 'following', select: 'username firstName lastName photoUrl' }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const settings = normalizeUserSettings(populatedUser.settings);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
user: {
|
user: {
|
||||||
|
|
@ -290,6 +296,8 @@ router.post('/session', async (req, res) => {
|
||||||
role: populatedUser.role,
|
role: populatedUser.role,
|
||||||
followersCount: populatedUser.followers.length,
|
followersCount: populatedUser.followers.length,
|
||||||
followingCount: populatedUser.following.length,
|
followingCount: populatedUser.following.length,
|
||||||
|
referralCode: populatedUser.referralCode,
|
||||||
|
referralsCount: populatedUser.referralsCount || 0,
|
||||||
settings,
|
settings,
|
||||||
banned: populatedUser.banned
|
banned: populatedUser.banned
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,5 +57,36 @@ router.post('/send-photos', authenticate, async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Отправить сообщение всем пользователям (только админы)
|
||||||
|
router.post('/broadcast', authenticate, async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Проверка прав админа
|
||||||
|
if (req.user.role !== 'admin') {
|
||||||
|
return res.status(403).json({ error: 'Требуются права администратора' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { message } = req.body;
|
||||||
|
|
||||||
|
if (!message || !message.trim()) {
|
||||||
|
return res.status(400).json({ error: 'Сообщение обязательно' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sendMessageToAllUsers } = require('../bots/mainBot');
|
||||||
|
const result = await sendMessageToAllUsers(message);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `Сообщение отправлено ${result.sent} пользователям`,
|
||||||
|
result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка рассылки:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Ошибка отправки сообщений',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,8 @@ const serializeUser = (user) => ({
|
||||||
banned: user.banned,
|
banned: user.banned,
|
||||||
bannedUntil: user.bannedUntil,
|
bannedUntil: user.bannedUntil,
|
||||||
lastActiveAt: user.lastActiveAt,
|
lastActiveAt: user.lastActiveAt,
|
||||||
createdAt: user.createdAt
|
createdAt: user.createdAt,
|
||||||
|
referralsCount: user.referralsCount || 0
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/auth/verify', authenticateModeration, requireModerationAccess, async (req, res) => {
|
router.post('/auth/verify', authenticateModeration, requireModerationAccess, async (req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,17 @@ router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploa
|
||||||
await post.save();
|
await post.save();
|
||||||
await post.populate('author', 'username firstName lastName photoUrl');
|
await post.populate('author', 'username firstName lastName photoUrl');
|
||||||
|
|
||||||
|
// Проверка первого поста для реферальной системы
|
||||||
|
// Счетчик рефералов увеличивается только когда приглашенный пользователь создал первый пост
|
||||||
|
const userPostsCount = await Post.countDocuments({ author: req.user._id });
|
||||||
|
if (userPostsCount === 1 && req.user.referredBy) {
|
||||||
|
// Это первый пост пользователя, который был приглашен по реферальной ссылке
|
||||||
|
const User = require('../models/User');
|
||||||
|
await User.findByIdAndUpdate(req.user.referredBy, {
|
||||||
|
$inc: { referralsCount: 1 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Создать уведомления для упомянутых пользователей
|
// Создать уведомления для упомянутых пользователей
|
||||||
if (post.mentionedUsers.length > 0) {
|
if (post.mentionedUsers.length > 0) {
|
||||||
const notifications = post.mentionedUsers.map(userId => ({
|
const notifications = post.mentionedUsers.map(userId => ({
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,8 @@ initWebSocket(server);
|
||||||
// Автообновление аватарок отключено - обновление происходит только при перезаходе
|
// Автообновление аватарок отключено - обновление происходит только при перезаходе
|
||||||
// scheduleAvatarUpdates();
|
// scheduleAvatarUpdates();
|
||||||
startServerMonitorBot();
|
startServerMonitorBot();
|
||||||
|
const { startMainBot } = require('./bots/mainBot');
|
||||||
|
startMainBot();
|
||||||
|
|
||||||
// Обработка необработанных ошибок
|
// Обработка необработанных ошибок
|
||||||
process.on('uncaughtException', (error) => {
|
process.on('uncaughtException', (error) => {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Heart, MessageCircle, MoreVertical, ChevronLeft, ChevronRight, Download, Send, X, ZoomIn } from 'lucide-react'
|
import { Heart, MessageCircle, MoreVertical, ChevronLeft, ChevronRight, Download, Send, X, ZoomIn, Share2 } from 'lucide-react'
|
||||||
import { likePost, deletePost, sendPhotoToTelegram } from '../utils/api'
|
import { likePost, deletePost, sendPhotoToTelegram } from '../utils/api'
|
||||||
import { hapticFeedback, showConfirm } from '../utils/telegram'
|
import { hapticFeedback, showConfirm, openTelegramLink } from '../utils/telegram'
|
||||||
import './PostCard.css'
|
import './PostCard.css'
|
||||||
|
|
||||||
const TAG_COLORS = {
|
const TAG_COLORS = {
|
||||||
|
|
@ -114,6 +114,27 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRepost = () => {
|
||||||
|
try {
|
||||||
|
hapticFeedback('light')
|
||||||
|
|
||||||
|
// Получить имя бота из переменных окружения или использовать дефолтное
|
||||||
|
const botName = import.meta.env.VITE_TELEGRAM_BOT_NAME || 'NakamaSpaceBot'
|
||||||
|
|
||||||
|
// Создать deeplink для открытия поста в миниапп
|
||||||
|
const deeplink = `https://t.me/${botName}?startapp=post_${post._id}`
|
||||||
|
|
||||||
|
// Открыть нативное окно "Поделиться" в Telegram
|
||||||
|
const shareUrl = `https://t.me/share/url?url=${encodeURIComponent(deeplink)}&text=${encodeURIComponent('Смотри пост в Nakama!')}`
|
||||||
|
|
||||||
|
openTelegramLink(shareUrl)
|
||||||
|
hapticFeedback('success')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка репоста:', error)
|
||||||
|
hapticFeedback('error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="post-card card fade-in">
|
<div className="post-card card fade-in">
|
||||||
{/* Хедер поста */}
|
{/* Хедер поста */}
|
||||||
|
|
@ -236,6 +257,17 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
||||||
<span>{post.comments.length}</span>
|
<span>{post.comments.length}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="action-btn"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleRepost()
|
||||||
|
}}
|
||||||
|
title="Поделиться постом"
|
||||||
|
>
|
||||||
|
<Share2 size={20} stroke="currentColor" />
|
||||||
|
</button>
|
||||||
|
|
||||||
{images.length > 0 && (
|
{images.length > 0 && (
|
||||||
<button
|
<button
|
||||||
className="action-btn"
|
className="action-btn"
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,96 @@
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Реферальная карточка */
|
||||||
|
.referral-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(52, 199, 89, 0.12);
|
||||||
|
color: #34C759;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-text h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-text p {
|
||||||
|
margin: 4px 0 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-stats {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-stats strong {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-link-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-link {
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-link code {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-copy-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--button-accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referral-copy-btn:active {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
.search-switch {
|
.search-switch {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Settings, Heart, Edit2, Shield } from 'lucide-react'
|
import { Settings, Heart, Edit2, Shield, Copy, Users } from 'lucide-react'
|
||||||
import { updateProfile } from '../utils/api'
|
import { updateProfile } from '../utils/api'
|
||||||
import { hapticFeedback } from '../utils/telegram'
|
import { hapticFeedback, showAlert } from '../utils/telegram'
|
||||||
import ThemeToggle from '../components/ThemeToggle'
|
import ThemeToggle from '../components/ThemeToggle'
|
||||||
import './Profile.css'
|
import './Profile.css'
|
||||||
|
|
||||||
|
|
@ -181,6 +181,62 @@ export default function Profile({ user, setUser }) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Реферальная ссылка */}
|
||||||
|
{user.referralCode && (
|
||||||
|
<div className="referral-card card">
|
||||||
|
<div className="referral-content">
|
||||||
|
<div className="referral-icon">
|
||||||
|
<Users size={20} />
|
||||||
|
</div>
|
||||||
|
<div className="referral-text">
|
||||||
|
<h3>Пригласи друзей</h3>
|
||||||
|
<p>Получи +1 к счетчику, когда приглашенный создаст первый пост</p>
|
||||||
|
<div className="referral-stats">
|
||||||
|
Приглашено: <strong>{user.referralsCount || 0}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="referral-link-section">
|
||||||
|
<div className="referral-link">
|
||||||
|
<code>{`https://t.me/${import.meta.env.VITE_TELEGRAM_BOT_NAME || 'NakamaSpaceBot'}?startapp=${user.referralCode}`}</code>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="referral-copy-btn"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
hapticFeedback('light')
|
||||||
|
const botName = import.meta.env.VITE_TELEGRAM_BOT_NAME || 'NakamaSpaceBot'
|
||||||
|
const referralLink = `https://t.me/${botName}?startapp=${user.referralCode}`
|
||||||
|
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
await navigator.clipboard.writeText(referralLink)
|
||||||
|
hapticFeedback('success')
|
||||||
|
showAlert('✅ Ссылка скопирована!')
|
||||||
|
} else {
|
||||||
|
const textArea = document.createElement('textarea')
|
||||||
|
textArea.value = referralLink
|
||||||
|
textArea.style.position = 'fixed'
|
||||||
|
textArea.style.opacity = '0'
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(textArea)
|
||||||
|
hapticFeedback('success')
|
||||||
|
showAlert('✅ Ссылка скопирована!')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка копирования:', error)
|
||||||
|
hapticFeedback('error')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy size={18} />
|
||||||
|
<span>Копировать</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="profile-powered">
|
<div className="profile-powered">
|
||||||
Powered by glpshcn \\ RBach \\ E621 \\ GelBooru
|
Powered by glpshcn \\ RBach \\ E621 \\ GelBooru
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -496,6 +496,7 @@ export default function App() {
|
||||||
<div className="list-item-meta">
|
<div className="list-item-meta">
|
||||||
<span>Роль: {u.role}</span>
|
<span>Роль: {u.role}</span>
|
||||||
<span>Активность: {formatDate(u.lastActiveAt)}</span>
|
<span>Активность: {formatDate(u.lastActiveAt)}</span>
|
||||||
|
{u.referralsCount > 0 && <span className="badge badge-info">Рефералов: {u.referralsCount}</span>}
|
||||||
{u.banned && <span className="badge badge-danger">Бан до {formatDate(u.bannedUntil)}</span>}
|
{u.banned && <span className="badge badge-danger">Бан до {formatDate(u.bannedUntil)}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue