Update files
This commit is contained in:
parent
73fa4a6ded
commit
4f39cb38fe
|
|
@ -172,96 +172,16 @@ 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) {
|
|
||||||
const trimmedParam = startParam.trim();
|
|
||||||
// Проверяем регистронезависимо (может быть ref_ или REF_)
|
|
||||||
const normalizedStartParam = trimmedParam.toLowerCase();
|
|
||||||
|
|
||||||
if (normalizedStartParam.startsWith('ref_')) {
|
|
||||||
// Убираем все префиксы ref_/REF_ (может быть двойной префикс ref_REF_)
|
|
||||||
let codeToSearch = trimmedParam;
|
|
||||||
while (codeToSearch.toLowerCase().startsWith('ref_')) {
|
|
||||||
codeToSearch = codeToSearch.substring(4); // Убираем "ref_" или "REF_"
|
|
||||||
}
|
|
||||||
|
|
||||||
// referralCode в базе хранится с префиксом "REF_" (в верхнем регистре)
|
|
||||||
// Ищем по коду с префиксом REF_
|
|
||||||
const escapedCode = codeToSearch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
const referrer = await User.findOne({
|
|
||||||
referralCode: { $regex: new RegExp(`^REF_${escapedCode}$`, 'i') }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (referrer) {
|
|
||||||
referredBy = referrer._id;
|
|
||||||
console.log(`🔗 Реферальная ссылка: пользователь ${normalizedUser.username || normalizedUser.id} зарегистрирован по ссылке от ${referrer.username} (${referrer._id}), код: ${trimmedParam} -> REF_${codeToSearch}`);
|
|
||||||
} else {
|
|
||||||
// Дополнительная проверка: посмотрим все referralCode в базе для отладки
|
|
||||||
const allCodes = await User.find({ referralCode: { $exists: true } }, { referralCode: 1, username: 1 }).limit(5);
|
|
||||||
console.warn(`⚠️ Реферальный код не найден: ${trimmedParam} (искали: REF_${codeToSearch})`);
|
|
||||||
console.warn(` Примеры кодов в базе: ${allCodes.map(u => u.referralCode).join(', ')}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`ℹ️ startParam не содержит ref_: ${trimmedParam}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
||||||
|
console.log(`✅ Создан новый пользователь ${user.username} (${user._id})`);
|
||||||
if (referredBy) {
|
|
||||||
console.log(`✅ Создан новый пользователь ${user.username} (${user._id}) с referredBy: ${referredBy}`);
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Создан новый пользователь ${user.username} (${user._id}) без реферала`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Счетчик рефералов увеличивается только когда пользователь создаст первый пост
|
|
||||||
// (см. routes/posts.js)
|
|
||||||
} else {
|
} else {
|
||||||
// Для существующих пользователей тоже можно установить referredBy,
|
|
||||||
// если они еще не были засчитаны как реферал и пришли по реферальной ссылке
|
|
||||||
if (startParam && !user.referredBy && !user.referralCounted) {
|
|
||||||
const trimmedParam = startParam.trim();
|
|
||||||
const normalizedStartParam = trimmedParam.toLowerCase();
|
|
||||||
|
|
||||||
if (normalizedStartParam.startsWith('ref_')) {
|
|
||||||
// Ищем по полному коду (с префиксом ref_)
|
|
||||||
const referrer = await User.findOne({
|
|
||||||
referralCode: { $regex: new RegExp(`^${trimmedParam.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (referrer) {
|
|
||||||
user.referredBy = referrer._id;
|
|
||||||
await user.save();
|
|
||||||
console.log(`🔗 Установлен referredBy для существующего пользователя ${user.username} от ${referrer.username} (код: ${trimmedParam})`);
|
|
||||||
} else {
|
|
||||||
// Попробуем альтернативный поиск
|
|
||||||
const codeWithoutPrefix = trimmedParam.substring(4);
|
|
||||||
const alternativeSearch = await User.findOne({
|
|
||||||
$or: [
|
|
||||||
{ referralCode: { $regex: new RegExp(`^ref_${codeWithoutPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') } },
|
|
||||||
{ referralCode: { $regex: new RegExp(`^REF_${codeWithoutPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') } }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (alternativeSearch) {
|
|
||||||
user.referredBy = alternativeSearch._id;
|
|
||||||
await user.save();
|
|
||||||
console.log(`🔗 Установлен referredBy (альтернативный поиск) для существующего пользователя ${user.username} от ${alternativeSearch.username}`);
|
|
||||||
} else {
|
|
||||||
console.warn(`⚠️ Реферальный код не найден для существующего пользователя ${user.username}: ${trimmedParam}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
|
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
|
||||||
if (normalizedUser.username) {
|
if (normalizedUser.username) {
|
||||||
user.username = normalizedUser.username;
|
user.username = normalizedUser.username;
|
||||||
|
|
@ -295,81 +215,6 @@ const authenticate = async (req, res, next) => {
|
||||||
await ensureUserSettings(user);
|
await ensureUserSettings(user);
|
||||||
await touchUserActivity(user);
|
await touchUserActivity(user);
|
||||||
|
|
||||||
// Реферальная система: отслеживание входов в разные дни
|
|
||||||
// Останавливаем отслеживание после засчета реферала, чтобы не заполнять БД
|
|
||||||
if (user.referredBy && !user.referralCounted) {
|
|
||||||
// Инициализировать loginDates если его нет
|
|
||||||
if (!user.loginDates) {
|
|
||||||
user.loginDates = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { getMoscowStartOfDay, getMoscowDate } = require('../utils/moscowTime');
|
|
||||||
const today = getMoscowDate();
|
|
||||||
const todayNormalized = getMoscowStartOfDay(today);
|
|
||||||
const todayTime = todayNormalized.getTime();
|
|
||||||
|
|
||||||
// Получить уникальные даты из существующего массива (только даты, без времени по московскому времени)
|
|
||||||
const uniqueDates = new Set();
|
|
||||||
if (user.loginDates && Array.isArray(user.loginDates)) {
|
|
||||||
user.loginDates.forEach(date => {
|
|
||||||
if (!date) return;
|
|
||||||
try {
|
|
||||||
// Нормализуем дату: получаем начало дня по московскому времени
|
|
||||||
const dateObj = new Date(date);
|
|
||||||
const normalizedDate = getMoscowStartOfDay(dateObj);
|
|
||||||
const normalizedTime = normalizedDate.getTime();
|
|
||||||
uniqueDates.add(normalizedTime);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Ошибка обработки даты ${date}:`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавить сегодняшнюю дату, если её еще нет
|
|
||||||
const todayExists = uniqueDates.has(todayTime);
|
|
||||||
if (!todayExists) {
|
|
||||||
if (!user.loginDates) {
|
|
||||||
user.loginDates = [];
|
|
||||||
}
|
|
||||||
user.loginDates.push(today);
|
|
||||||
uniqueDates.add(todayTime);
|
|
||||||
console.log(`📅 Реферал ${user.username} (${user._id}): добавлена дата входа. Уникальных дат: ${uniqueDates.size}/2`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если уже есть 2 или более уникальные даты, засчитать реферала
|
|
||||||
if (uniqueDates.size >= 2) {
|
|
||||||
const User = require('../models/User');
|
|
||||||
const referrer = await User.findById(user.referredBy);
|
|
||||||
|
|
||||||
if (!referrer) {
|
|
||||||
console.error(`❌ Реферер не найден для пользователя ${user.username} (${user._id}), referredBy: ${user.referredBy}`);
|
|
||||||
} else {
|
|
||||||
// Увеличить счетчик рефералов
|
|
||||||
const oldCount = referrer.referralsCount || 0;
|
|
||||||
referrer.referralsCount = oldCount + 1;
|
|
||||||
await referrer.save();
|
|
||||||
|
|
||||||
console.log(`✅ Реферал засчитан: пользователь ${user.username} (${user._id}) засчитан для ${referrer.username} (${referrer._id}). Счетчик: ${oldCount} -> ${referrer.referralsCount}`);
|
|
||||||
|
|
||||||
// Начислить баллы за реферала
|
|
||||||
try {
|
|
||||||
const { awardReferral } = require('../utils/tickets');
|
|
||||||
await awardReferral(user.referredBy);
|
|
||||||
console.log(` ✅ Баллы начислены рефереру ${referrer.username}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(` ⚠️ Ошибка начисления баллов:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user.referralCounted = true;
|
|
||||||
// Очистить loginDates после засчета, чтобы не хранить лишние данные
|
|
||||||
user.loginDates = [];
|
|
||||||
await user.save();
|
|
||||||
} else {
|
|
||||||
// Если еще нет 2 уникальных дат, сохранить обновленный массив loginDates
|
|
||||||
await user.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user = user;
|
req.user = user;
|
||||||
req.telegramUser = normalizedUser;
|
req.telegramUser = normalizedUser;
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,6 @@ const TicketActivitySchema = new mongoose.Schema({
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
},
|
},
|
||||||
referralsCounted: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
// Для отслеживания баллов с реакций на арты (по постам)
|
// Для отслеживания баллов с реакций на арты (по постам)
|
||||||
// Используем объект вместо Map для совместимости с Mongoose
|
// Используем объект вместо Map для совместимости с Mongoose
|
||||||
artReactionsPoints: {
|
artReactionsPoints: {
|
||||||
|
|
|
||||||
|
|
@ -84,29 +84,6 @@ 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
|
|
||||||
},
|
|
||||||
// Массив дат входа (для реферальной системы)
|
|
||||||
loginDates: [{
|
|
||||||
type: Date
|
|
||||||
}],
|
|
||||||
// Флаг, что реферал уже засчитан
|
|
||||||
referralCounted: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
// Билеты для Monthly Ladder
|
// Билеты для Monthly Ladder
|
||||||
tickets: {
|
tickets: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
|
@ -137,15 +114,5 @@ const UserSchema = new mongoose.Schema({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Генерировать реферальный код перед сохранением
|
|
||||||
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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,6 @@ const respondWithUser = async (user, res) => {
|
||||||
followingCount: populatedUser.following.length,
|
followingCount: populatedUser.following.length,
|
||||||
followers: populatedUser.followers,
|
followers: populatedUser.followers,
|
||||||
following: populatedUser.following,
|
following: populatedUser.following,
|
||||||
referralCode: populatedUser.referralCode,
|
|
||||||
referralsCount: populatedUser.referralsCount || 0,
|
|
||||||
tickets: populatedUser.tickets || 0,
|
tickets: populatedUser.tickets || 0,
|
||||||
settings,
|
settings,
|
||||||
banned: populatedUser.banned
|
banned: populatedUser.banned
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,6 @@ const serializeUser = (user) => {
|
||||||
bannedUntil: user.bannedUntil,
|
bannedUntil: user.bannedUntil,
|
||||||
lastActiveAt: user.lastActiveAt,
|
lastActiveAt: user.lastActiveAt,
|
||||||
createdAt: user.createdAt,
|
createdAt: user.createdAt,
|
||||||
referralsCount: user.referralsCount || 0,
|
|
||||||
// passwordHash никогда не возвращается (уже select: false в модели)
|
// passwordHash никогда не возвращается (уже select: false в модели)
|
||||||
// email не возвращается для безопасности
|
// email не возвращается для безопасности
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -204,73 +204,5 @@ router.get('/search/:query', authenticate, async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Получить топ пользователей по билетам (Monthly Ladder)
|
|
||||||
router.get('/ladder/top', authenticate, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { limit = 5 } = req.query;
|
|
||||||
const limitNum = parseInt(limit);
|
|
||||||
|
|
||||||
// Получить топ пользователей по билетам (исключаем glpshchn00)
|
|
||||||
// Берем немного больше, чтобы гарантировать топ 5 после фильтрации
|
|
||||||
const topUsers = await User.find({
|
|
||||||
banned: { $ne: true },
|
|
||||||
username: { $ne: 'glpshchn00' }
|
|
||||||
})
|
|
||||||
.select('username firstName lastName photoUrl tickets')
|
|
||||||
.sort({ tickets: -1 })
|
|
||||||
.limit(limitNum)
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
// Найти позицию текущего пользователя (исключаем glpshchn00)
|
|
||||||
const userTickets = req.user.tickets || 0;
|
|
||||||
const userRank = await User.countDocuments({
|
|
||||||
tickets: { $gt: userTickets },
|
|
||||||
banned: { $ne: true },
|
|
||||||
username: { $ne: 'glpshchn00' }
|
|
||||||
}) + 1;
|
|
||||||
|
|
||||||
// Исключить glpshchn00 из отображения
|
|
||||||
const isExcludedUser = req.user.username === 'glpshchn00';
|
|
||||||
|
|
||||||
// Проверить, есть ли текущий пользователь в топе
|
|
||||||
const currentUserInTop = !isExcludedUser && topUsers.some(u => u._id.toString() === req.user._id.toString());
|
|
||||||
|
|
||||||
// Если пользователь не в топе и не исключен, добавить его отдельно
|
|
||||||
let currentUserData = null;
|
|
||||||
if (!isExcludedUser && !currentUserInTop) {
|
|
||||||
currentUserData = {
|
|
||||||
_id: req.user._id,
|
|
||||||
username: req.user.username,
|
|
||||||
firstName: req.user.firstName,
|
|
||||||
lastName: req.user.lastName,
|
|
||||||
photoUrl: req.user.photoUrl,
|
|
||||||
tickets: req.user.tickets || 0,
|
|
||||||
rank: userRank
|
|
||||||
};
|
|
||||||
} else if (!isExcludedUser && currentUserInTop) {
|
|
||||||
// Если в топе, добавить rank к существующему пользователю
|
|
||||||
topUsers.forEach((user, index) => {
|
|
||||||
if (user._id.toString() === req.user._id.toString()) {
|
|
||||||
user.rank = index + 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавить rank к топ пользователям
|
|
||||||
topUsers.forEach((user, index) => {
|
|
||||||
user.rank = index + 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
topUsers,
|
|
||||||
currentUser: currentUserData,
|
|
||||||
currentUserRank: isExcludedUser ? null : (currentUserInTop ? topUsers.find(u => u._id.toString() === req.user._id.toString())?.rank : userRank)
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка получения топа:', error);
|
|
||||||
res.status(500).json({ error: 'Ошибка сервера' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,6 @@ async function canAwardTickets(userId, actionType, options = {}) {
|
||||||
case 'comment_received':
|
case 'comment_received':
|
||||||
return true; // Нет лимита на получение комментариев
|
return true; // Нет лимита на получение комментариев
|
||||||
|
|
||||||
case 'referral':
|
|
||||||
return activity.referralsCounted < 3;
|
|
||||||
|
|
||||||
case 'art_reaction':
|
case 'art_reaction':
|
||||||
// Проверка лимита на реакцию на арт (100 баллов в сутки с одного арта)
|
// Проверка лимита на реакцию на арт (100 баллов в сутки с одного арта)
|
||||||
const postId = options.postId;
|
const postId = options.postId;
|
||||||
|
|
@ -108,9 +105,6 @@ async function awardTickets(userId, points, actionType, options = {}) {
|
||||||
case 'comment_received':
|
case 'comment_received':
|
||||||
activity.commentsReceived += 1;
|
activity.commentsReceived += 1;
|
||||||
break;
|
break;
|
||||||
case 'referral':
|
|
||||||
activity.referralsCounted += 1;
|
|
||||||
break;
|
|
||||||
case 'art_reaction':
|
case 'art_reaction':
|
||||||
const postId = options.postId;
|
const postId = options.postId;
|
||||||
if (postId) {
|
if (postId) {
|
||||||
|
|
@ -176,13 +170,6 @@ async function awardCommentReceived(authorId, commenterId) {
|
||||||
return await awardTickets(authorId, 6, 'comment_received', { senderId: commenterId });
|
return await awardTickets(authorId, 6, 'comment_received', { senderId: commenterId });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Начисляет баллы за реферала
|
|
||||||
*/
|
|
||||||
async function awardReferral(userId) {
|
|
||||||
return await awardTickets(userId, 100, 'referral');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Начисляет баллы за реакцию на арт (лайк)
|
* Начисляет баллы за реакцию на арт (лайк)
|
||||||
*/
|
*/
|
||||||
|
|
@ -469,7 +456,6 @@ module.exports = {
|
||||||
awardLikeReceived,
|
awardLikeReceived,
|
||||||
awardCommentWritten,
|
awardCommentWritten,
|
||||||
awardCommentReceived,
|
awardCommentReceived,
|
||||||
awardReferral,
|
|
||||||
awardArtLike,
|
awardArtLike,
|
||||||
awardArtComment,
|
awardArtComment,
|
||||||
awardArtModeration,
|
awardArtModeration,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import Profile from './pages/Profile'
|
||||||
import UserProfile from './pages/UserProfile'
|
import UserProfile from './pages/UserProfile'
|
||||||
import CommentsPage from './pages/CommentsPage'
|
import CommentsPage from './pages/CommentsPage'
|
||||||
import PostMenuPage from './pages/PostMenuPage'
|
import PostMenuPage from './pages/PostMenuPage'
|
||||||
import MonthlyLadder from './pages/MonthlyLadder'
|
|
||||||
import MiniPlayer from './components/MiniPlayer'
|
import MiniPlayer from './components/MiniPlayer'
|
||||||
import FullPlayer from './components/FullPlayer'
|
import FullPlayer from './components/FullPlayer'
|
||||||
import './styles/index.css'
|
import './styles/index.css'
|
||||||
|
|
@ -233,7 +232,6 @@ function AppContent() {
|
||||||
<Route path="user/:id" element={<UserProfile currentUser={user} />} />
|
<Route path="user/:id" element={<UserProfile currentUser={user} />} />
|
||||||
<Route path="post/:postId/comments" element={<CommentsPage user={user} />} />
|
<Route path="post/:postId/comments" element={<CommentsPage user={user} />} />
|
||||||
<Route path="post/:postId/menu" element={<PostMenuPage user={user} />} />
|
<Route path="post/:postId/menu" element={<PostMenuPage user={user} />} />
|
||||||
<Route path="ladder" element={<MonthlyLadder user={user} />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
.ladder-button {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 140px;
|
|
||||||
right: 16px;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: linear-gradient(135deg, #FFD700, #FFA500, #FF6347);
|
|
||||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
|
||||||
box-shadow: 0 4px 20px rgba(255, 215, 0, 0.4),
|
|
||||||
0 0 30px rgba(255, 165, 0, 0.3);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 100;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
animation: float 3s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ladder-button:active {
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ladder-button:hover {
|
|
||||||
box-shadow: 0 6px 30px rgba(255, 215, 0, 0.6),
|
|
||||||
0 0 40px rgba(255, 165, 0, 0.5);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
0%, 100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gift-icon {
|
|
||||||
color: #fff;
|
|
||||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
|
|
||||||
animation: rotate 4s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotate {
|
|
||||||
0%, 100% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: rotate(-10deg);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: rotate(10deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптация для темной темы */
|
|
||||||
[data-theme="dark"] .ladder-button {
|
|
||||||
box-shadow: 0 4px 20px rgba(255, 215, 0, 0.5),
|
|
||||||
0 0 30px rgba(255, 165, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
|
||||||
import { Gift } from 'lucide-react'
|
|
||||||
import { hapticFeedback } from '../utils/telegram'
|
|
||||||
import './LadderButton.css'
|
|
||||||
|
|
||||||
export default function LadderButton() {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
// Скрыть кнопку на странице ladder и profile
|
|
||||||
if (location.pathname === '/ladder' || location.pathname === '/profile') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
hapticFeedback('light')
|
|
||||||
navigate('/ladder')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button className="ladder-button" onClick={handleClick} aria-label="Monthly Ladder">
|
|
||||||
<Gift size={24} className="gift-icon" />
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Outlet } from 'react-router-dom'
|
import { Outlet } from 'react-router-dom'
|
||||||
import Navigation from './Navigation'
|
import Navigation from './Navigation'
|
||||||
import LadderButton from './LadderButton'
|
|
||||||
import './Layout.css'
|
import './Layout.css'
|
||||||
|
|
||||||
export default function Layout({ user }) {
|
export default function Layout({ user }) {
|
||||||
|
|
@ -10,7 +9,6 @@ export default function Layout({ user }) {
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<LadderButton />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,571 +0,0 @@
|
||||||
.ladder-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
position: relative;
|
|
||||||
overflow-x: hidden;
|
|
||||||
padding-bottom: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Новогодние снежинки - тонкие, не навязчивые */
|
|
||||||
.new-year-decorations {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake {
|
|
||||||
position: absolute;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 16px;
|
|
||||||
animation: fall linear infinite;
|
|
||||||
animation-duration: 15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-child(1) {
|
|
||||||
left: 10%;
|
|
||||||
animation-delay: 0s;
|
|
||||||
animation-duration: 12s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-child(2) {
|
|
||||||
left: 30%;
|
|
||||||
animation-delay: 2s;
|
|
||||||
animation-duration: 18s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-child(3) {
|
|
||||||
left: 50%;
|
|
||||||
animation-delay: 4s;
|
|
||||||
animation-duration: 14s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-child(4) {
|
|
||||||
left: 70%;
|
|
||||||
animation-delay: 1s;
|
|
||||||
animation-duration: 16s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-child(5) {
|
|
||||||
left: 85%;
|
|
||||||
animation-delay: 3s;
|
|
||||||
animation-duration: 17s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snowflake:nth-child(6) {
|
|
||||||
left: 20%;
|
|
||||||
animation-delay: 5s;
|
|
||||||
animation-duration: 20s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fall {
|
|
||||||
0% {
|
|
||||||
transform: translateY(-100vh) rotate(0deg);
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(100vh) rotate(360deg);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ladder-header {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ladder-header h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-btn {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-primary);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Карточка отсчета - в стиле Nakama с новогодними акцентами */
|
|
||||||
.countdown-card {
|
|
||||||
margin: 16px;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 2px 8px var(--shadow-md);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(var(--bg-secondary), var(--bg-secondary)),
|
|
||||||
linear-gradient(135deg, #FFD700, #FFA500);
|
|
||||||
background-origin: border-box;
|
|
||||||
background-clip: padding-box, border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown-title h2 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gift-icon {
|
|
||||||
color: #FF6347;
|
|
||||||
animation: bounce 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce {
|
|
||||||
0%, 100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown-timer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin: 20px 0;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown-value {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #FFD700;
|
|
||||||
min-width: 60px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown-separator {
|
|
||||||
font-size: 24px;
|
|
||||||
color: #FFD700;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown-slogan {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: italic;
|
|
||||||
margin-top: 16px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Топ пользователей - в стиле Nakama */
|
|
||||||
.ladder-top {
|
|
||||||
margin: 16px;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
box-shadow: 0 2px 8px var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ladder-top-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ladder-top-header h2 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 20px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-btn:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
background: var(--divider-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-btn svg {
|
|
||||||
color: #FFD700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-users-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-user-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: transparent;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
|
|
||||||
transition: all 0.2s;
|
|
||||||
min-height: 54px;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .top-user-item {
|
|
||||||
border-bottom-color: rgba(255, 255, 255, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-user-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-user-item.current-user {
|
|
||||||
background: rgba(255, 215, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-rank {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
min-width: 24px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-icon {
|
|
||||||
filter: drop-shadow(0 0 4px currentColor);
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-icon.gold {
|
|
||||||
color: #FFD700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-icon.silver {
|
|
||||||
color: #C0C0C0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-icon.bronze {
|
|
||||||
color: #CD7F32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-number {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-avatar {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
min-width: 44px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
border: none;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-name {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-stats {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 2px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
text-align: right;
|
|
||||||
max-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-tickets {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-prize {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-badge {
|
|
||||||
color: #FFD700;
|
|
||||||
filter: drop-shadow(0 0 4px #FFD700);
|
|
||||||
animation: twinkle 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes twinkle {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Карточка текущего пользователя */
|
|
||||||
.current-user-card {
|
|
||||||
margin: 16px;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
box-shadow: 0 2px 8px var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-user-card h3 {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-user-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 8px 0;
|
|
||||||
background: transparent;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
|
|
||||||
min-height: 54px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .current-user-item {
|
|
||||||
border-bottom-color: rgba(255, 255, 255, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Модальное окно с информацией */
|
|
||||||
.info-modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 10000;
|
|
||||||
padding: 20px;
|
|
||||||
animation: fadeIn 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-modal {
|
|
||||||
max-width: 500px;
|
|
||||||
max-height: 80vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 4px 20px var(--shadow-lg);
|
|
||||||
position: relative;
|
|
||||||
z-index: 10001;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px;
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
z-index: 10002;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-modal-header h2 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
border: none;
|
|
||||||
font-size: 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-modal-content {
|
|
||||||
padding: 16px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-section {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-section:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-section h3 {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-section p {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-limit {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-section.anti-fraud {
|
|
||||||
background: var(--bg-primary);
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-section.anti-fraud h3 {
|
|
||||||
color: #FF6347;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-state {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 3px solid var(--divider-color);
|
|
||||||
border-top-color: #FFD700;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Темная тема */
|
|
||||||
[data-theme="dark"] .countdown-card {
|
|
||||||
background-image:
|
|
||||||
linear-gradient(var(--bg-secondary), var(--bg-secondary)),
|
|
||||||
linear-gradient(135deg, #FFD700, #FFA500);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .snowflake {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .countdown-value {
|
|
||||||
color: #FFD700;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .countdown-separator {
|
|
||||||
color: #FFD700;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .user-tickets {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
@ -1,319 +0,0 @@
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { ChevronLeft, Info, Gift, Trophy, Star } from 'lucide-react'
|
|
||||||
import { getLadderTop } from '../utils/api'
|
|
||||||
import { hapticFeedback } from '../utils/telegram'
|
|
||||||
import './MonthlyLadder.css'
|
|
||||||
|
|
||||||
export default function MonthlyLadder({ user }) {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const [topUsers, setTopUsers] = useState([])
|
|
||||||
const [currentUser, setCurrentUser] = useState(null)
|
|
||||||
const [currentUserRank, setCurrentUserRank] = useState(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [showInfo, setShowInfo] = useState(false)
|
|
||||||
const [timeLeft, setTimeLeft] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 })
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadLadder()
|
|
||||||
updateCountdown()
|
|
||||||
const interval = setInterval(updateCountdown, 1000)
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const updateCountdown = () => {
|
|
||||||
// Получить текущее московское время
|
|
||||||
const getMoscowTime = () => {
|
|
||||||
const now = new Date()
|
|
||||||
// Москва = UTC+3
|
|
||||||
const moscowOffset = 3 * 60 * 60 * 1000 // 3 часа в миллисекундах
|
|
||||||
const utcTime = now.getTime() + (now.getTimezoneOffset() * 60 * 1000)
|
|
||||||
return new Date(utcTime + moscowOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получить новогоднюю дату по московскому времени (1 января следующего года, 00:00 MSK)
|
|
||||||
const getNewYearMoscow = () => {
|
|
||||||
const moscowNow = getMoscowTime()
|
|
||||||
const year = moscowNow.getFullYear() + 1
|
|
||||||
// Создаем дату 1 января следующего года в UTC, затем вычитаем смещение
|
|
||||||
const moscowNewYear = new Date(Date.UTC(year, 0, 1, 0, 0, 0, 0))
|
|
||||||
const moscowOffset = 3 * 60 * 60 * 1000
|
|
||||||
return new Date(moscowNewYear.getTime() - moscowOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = getMoscowTime()
|
|
||||||
const newYear = getNewYearMoscow()
|
|
||||||
const diff = newYear.getTime() - now.getTime()
|
|
||||||
|
|
||||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
|
||||||
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
|
||||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
|
||||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000)
|
|
||||||
|
|
||||||
setTimeLeft({ days, hours, minutes, seconds })
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadLadder = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
const data = await getLadderTop(5)
|
|
||||||
setTopUsers(data.topUsers || [])
|
|
||||||
setCurrentUser(data.currentUser)
|
|
||||||
setCurrentUserRank(data.currentUserRank)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка загрузки ладдера:', error)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRankIcon = (rank) => {
|
|
||||||
switch (rank) {
|
|
||||||
case 1:
|
|
||||||
return <Trophy size={24} className="rank-icon gold" />
|
|
||||||
case 2:
|
|
||||||
return <Trophy size={24} className="rank-icon silver" />
|
|
||||||
case 3:
|
|
||||||
return <Trophy size={24} className="rank-icon bronze" />
|
|
||||||
default:
|
|
||||||
return <span className="rank-number">{rank}</span>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPrize = (rank) => {
|
|
||||||
switch (rank) {
|
|
||||||
case 1:
|
|
||||||
return '$50'
|
|
||||||
case 2:
|
|
||||||
return '$30'
|
|
||||||
case 3:
|
|
||||||
return '$15'
|
|
||||||
case 4:
|
|
||||||
return '$5'
|
|
||||||
case 5:
|
|
||||||
return '$5'
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTickets = (tickets) => {
|
|
||||||
return tickets?.toLocaleString('ru-RU') || '0'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTicketsWord = (tickets) => {
|
|
||||||
const num = tickets || 0
|
|
||||||
const lastDigit = num % 10
|
|
||||||
const lastTwoDigits = num % 100
|
|
||||||
|
|
||||||
// Исключения для 11-14
|
|
||||||
if (lastTwoDigits >= 11 && lastTwoDigits <= 14) {
|
|
||||||
return 'билетов'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1, 21, 31, 41... - билет
|
|
||||||
if (lastDigit === 1) {
|
|
||||||
return 'билет'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2, 3, 4, 22, 23, 24... - билета
|
|
||||||
if (lastDigit >= 2 && lastDigit <= 4) {
|
|
||||||
return 'билета'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Остальные - билетов
|
|
||||||
return 'билетов'
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ladder-page">
|
|
||||||
{/* Хедер */}
|
|
||||||
<div className="ladder-header">
|
|
||||||
<button className="back-btn" onClick={() => navigate(-1)}>
|
|
||||||
<ChevronLeft size={24} />
|
|
||||||
</button>
|
|
||||||
<h1>Monthly Ladder</h1>
|
|
||||||
<div style={{ width: 44 }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Новогодний декор */}
|
|
||||||
<div className="new-year-decorations">
|
|
||||||
<div className="snowflake">❄️</div>
|
|
||||||
<div className="snowflake">❄️</div>
|
|
||||||
<div className="snowflake">❄️</div>
|
|
||||||
<div className="snowflake">❄️</div>
|
|
||||||
<div className="snowflake">❄️</div>
|
|
||||||
<div className="snowflake">❄️</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Отсчет до нового года */}
|
|
||||||
<div className="countdown-card">
|
|
||||||
<div className="countdown-title">
|
|
||||||
<Gift size={24} className="gift-icon" />
|
|
||||||
<h2>До Нового Года</h2>
|
|
||||||
</div>
|
|
||||||
<div className="countdown-timer">
|
|
||||||
<div className="countdown-item">
|
|
||||||
<span className="countdown-value">{timeLeft.days}</span>
|
|
||||||
<span className="countdown-label">дней</span>
|
|
||||||
</div>
|
|
||||||
<div className="countdown-separator">:</div>
|
|
||||||
<div className="countdown-item">
|
|
||||||
<span className="countdown-value">{String(timeLeft.hours).padStart(2, '0')}</span>
|
|
||||||
<span className="countdown-label">часов</span>
|
|
||||||
</div>
|
|
||||||
<div className="countdown-separator">:</div>
|
|
||||||
<div className="countdown-item">
|
|
||||||
<span className="countdown-value">{String(timeLeft.minutes).padStart(2, '0')}</span>
|
|
||||||
<span className="countdown-label">минут</span>
|
|
||||||
</div>
|
|
||||||
<div className="countdown-separator">:</div>
|
|
||||||
<div className="countdown-item">
|
|
||||||
<span className="countdown-value">{String(timeLeft.seconds).padStart(2, '0')}</span>
|
|
||||||
<span className="countdown-label">секунд</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="countdown-slogan">Ваши посты, ваши арты, ваша слава. Остальное потом.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Топ 5 пользователей */}
|
|
||||||
<div className="ladder-top">
|
|
||||||
<div className="ladder-top-header">
|
|
||||||
<h2>Топ 5</h2>
|
|
||||||
<button
|
|
||||||
className="info-btn"
|
|
||||||
onClick={() => {
|
|
||||||
setShowInfo(true)
|
|
||||||
hapticFeedback('light')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Info size={20} />
|
|
||||||
<span>За что начисляются билеты</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="loading-state">
|
|
||||||
<div className="spinner" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="top-users-list">
|
|
||||||
{topUsers.map((topUser, index) => {
|
|
||||||
const isCurrentUser = user && (topUser._id === user.id || topUser._id?.toString() === user.id?.toString())
|
|
||||||
const prize = getPrize(topUser.rank)
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={topUser._id}
|
|
||||||
className={`top-user-item ${isCurrentUser ? 'current-user' : ''}`}
|
|
||||||
>
|
|
||||||
<div className="user-rank">
|
|
||||||
{getRankIcon(topUser.rank)}
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
src={topUser.photoUrl || '/default-avatar.png'}
|
|
||||||
alt={topUser.username}
|
|
||||||
className="user-avatar"
|
|
||||||
/>
|
|
||||||
<div className="user-info">
|
|
||||||
<div className="user-name">
|
|
||||||
{topUser.firstName || topUser.username}
|
|
||||||
{isCurrentUser && <Star size={16} className="current-badge" />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="user-stats">
|
|
||||||
<span className="user-tickets">{formatTickets(topUser.tickets)} {getTicketsWord(topUser.tickets)}</span>
|
|
||||||
{prize && <span className="user-prize">{prize}</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Текущий пользователь (если не в топе) */}
|
|
||||||
{currentUser && currentUserRank > 5 && (
|
|
||||||
<div className="current-user-card">
|
|
||||||
<h3>Ваша позиция</h3>
|
|
||||||
<div className="current-user-item">
|
|
||||||
<div className="user-rank">
|
|
||||||
<span className="rank-number">{currentUserRank}</span>
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
src={currentUser.photoUrl || '/default-avatar.png'}
|
|
||||||
alt={currentUser.username}
|
|
||||||
className="user-avatar"
|
|
||||||
/>
|
|
||||||
<div className="user-info">
|
|
||||||
<div className="user-name">
|
|
||||||
{currentUser.firstName || currentUser.username}
|
|
||||||
<Star size={16} className="current-badge" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="user-stats">
|
|
||||||
<span className="user-tickets">{formatTickets(currentUser.tickets)} {getTicketsWord(currentUser.tickets)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Модальное окно с информацией */}
|
|
||||||
{showInfo && (
|
|
||||||
<div className="info-modal-overlay" onClick={() => setShowInfo(false)}>
|
|
||||||
<div className="info-modal" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<div className="info-modal-header">
|
|
||||||
<h2>За что начисляются билеты</h2>
|
|
||||||
<button className="close-btn" onClick={() => setShowInfo(false)}>×</button>
|
|
||||||
</div>
|
|
||||||
<div className="info-modal-content">
|
|
||||||
<div className="info-section">
|
|
||||||
<h3>1. Посты</h3>
|
|
||||||
<p>+15 билетов за создание поста</p>
|
|
||||||
<p className="info-limit">Лимит: 5 постов в день</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="info-section">
|
|
||||||
<h3>2. Лайки</h3>
|
|
||||||
<p><strong>Ставишь лайки:</strong> +1 билет за лайк</p>
|
|
||||||
<p className="info-limit">Лимит: 50 в день</p>
|
|
||||||
<p><strong>Получаешь лайки:</strong> +2 билета за лайк под твоей записью</p>
|
|
||||||
<p className="info-limit">Лимит учёта: 100 лайков в день</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="info-section">
|
|
||||||
<h3>3. Комментарии</h3>
|
|
||||||
<p><strong>Пишешь комментарии:</strong> +4 билета за комментарий длиной 10+ символов</p>
|
|
||||||
<p className="info-limit">Лимит: 20 комментариев в день</p>
|
|
||||||
<p><strong>Получаешь комментарии:</strong> +6 билетов за комментарий под твоим постом</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="info-section">
|
|
||||||
<h3>4. Рефералы</h3>
|
|
||||||
<p>+100 билетов за одного валидного реферала</p>
|
|
||||||
<p className="info-limit">Лимит: 3 реферала в день</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="info-section">
|
|
||||||
<h3>5. Ваше творчество (арты)</h3>
|
|
||||||
<p><strong>Публикация:</strong> +40 билетов за арт, прошедший модерацию</p>
|
|
||||||
<p className="info-limit">Лимит: 1 арт в день / 5 в неделю</p>
|
|
||||||
<p><strong>Реакции на арт:</strong></p>
|
|
||||||
<p>+8 билетов за лайк под артом</p>
|
|
||||||
<p>+12 билетов за комментарий под артом (1 комментарий от одного человека в сутки)</p>
|
|
||||||
<p className="info-limit">Лимит: до 100 билетов в сутки с реакций на один арт</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="info-section anti-fraud">
|
|
||||||
<h3>Немного правил</h3>
|
|
||||||
<p>Лайки/комменты от аккаунтов младше 24 часов не считаем</p>
|
|
||||||
<p>Комменты <10 символов = 0 билетов</p>
|
|
||||||
<p>Ограничение на билеты по входящим реакциям, чтобы боты не устроили ферму</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -184,96 +184,6 @@
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -318,18 +318,6 @@ export default function Profile({ user, setUser }) {
|
||||||
setSettings(updatedSettings)
|
setSettings(updatedSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCopyReferral = () => {
|
|
||||||
const botUsername = import.meta.env.VITE_BOT_USERNAME || 'NakamaSpaceBot'
|
|
||||||
const referralLink = `https://t.me/${botUsername}?startapp=ref_${user.referralCode || user.id}`
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(referralLink).then(() => {
|
|
||||||
hapticFeedback('success')
|
|
||||||
alert('Реферальная ссылка скопирована!')
|
|
||||||
}).catch(() => {
|
|
||||||
hapticFeedback('error')
|
|
||||||
alert('Не удалось скопировать ссылку')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="profile-page">
|
<div className="profile-page">
|
||||||
|
|
@ -405,33 +393,6 @@ export default function Profile({ user, setUser }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Реферальная карточка */}
|
|
||||||
{user.referralCode && (
|
|
||||||
<div className="referral-card card">
|
|
||||||
<div className="referral-content">
|
|
||||||
<div className="referral-icon">
|
|
||||||
<UserPlus size={20} />
|
|
||||||
</div>
|
|
||||||
<div className="referral-text">
|
|
||||||
<h3>Реферальная программа</h3>
|
|
||||||
<p>Приглашайте друзей и получайте бонусы за каждого приглашенного пользователя!</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_BOT_USERNAME || 'NakamaSpaceBot'}?startapp=ref_${user.referralCode}`}</code>
|
|
||||||
</div>
|
|
||||||
<button className="referral-copy-btn" onClick={handleCopyReferral}>
|
|
||||||
<Copy size={16} />
|
|
||||||
Скопировать ссылку
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Привязка email (только для Telegram пользователей без email) */}
|
{/* Привязка email (только для Telegram пользователей без email) */}
|
||||||
{user.telegramId && !user.email && (
|
{user.telegramId && !user.email && (
|
||||||
<div className="link-email-card card">
|
<div className="link-email-card card">
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,34 @@ export const verifyAuth = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Авторизация через Telegram OAuth (Login Widget)
|
// Авторизация через Telegram OAuth (Login Widget)
|
||||||
|
|
||||||
|
// Magic-link авторизация
|
||||||
|
export const sendMagicLink = async (email) => {
|
||||||
|
const response = await api.post('/auth/magic-link/send', { email })
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const verifyMagicLink = async (token) => {
|
||||||
|
const response = await api.get('/auth/magic-link/verify', { params: { token } })
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setPassword = async (token, password, username) => {
|
||||||
|
const response = await api.post('/auth/magic-link/set-password', { token, password, username })
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Привязка аккаунтов
|
||||||
|
export const linkTelegram = async (initData) => {
|
||||||
|
const response = await api.post('/auth/link-telegram', { initData })
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const linkEmail = async (email, password) => {
|
||||||
|
const response = await api.post('/auth/link-email', { email, password })
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
// Posts API
|
// Posts API
|
||||||
export const getPosts = async (params = {}) => {
|
export const getPosts = async (params = {}) => {
|
||||||
// Поддержка старого формата для обратной совместимости
|
// Поддержка старого формата для обратной совместимости
|
||||||
|
|
@ -205,12 +233,6 @@ export const searchUsers = async (query) => {
|
||||||
return response.data.users
|
return response.data.users
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ladder API
|
|
||||||
export const getLadderTop = async (limit = 5) => {
|
|
||||||
const response = await api.get('/users/ladder/top', { params: { limit } })
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notifications API
|
// Notifications API
|
||||||
export const getNotifications = async (params = {}) => {
|
export const getNotifications = async (params = {}) => {
|
||||||
const response = await api.get('/notifications', { params })
|
const response = await api.get('/notifications', { params })
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,6 @@ async def get_users(
|
||||||
'bannedUntil': banned_until,
|
'bannedUntil': banned_until,
|
||||||
'createdAt': created_at,
|
'createdAt': created_at,
|
||||||
'lastActiveAt': last_active_at,
|
'lastActiveAt': last_active_at,
|
||||||
'referralsCount': int(u.get('referralsCount', 0)),
|
|
||||||
'isAdmin': is_admin
|
'isAdmin': is_admin
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1009,7 +1009,6 @@ 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