From 4f39cb38fe4ebed38f9291b79428fb1e6c77b264 Mon Sep 17 00:00:00 2001
From: glpshchn <464976@niuitmo.ru>
Date: Thu, 1 Jan 2026 21:52:37 +0300
Subject: [PATCH] Update files
---
backend/middleware/auth.js | 159 +------
backend/models/TicketActivity.js | 4 -
backend/models/User.js | 33 --
backend/routes/auth.js | 2 -
backend/routes/modApp.js | 1 -
backend/routes/users.js | 68 ---
backend/utils/tickets.js | 14 -
frontend/src/App.jsx | 2 -
frontend/src/components/LadderButton.css | 65 ---
frontend/src/components/LadderButton.jsx | 26 --
frontend/src/components/Layout.jsx | 2 -
frontend/src/pages/MonthlyLadder.css | 571 -----------------------
frontend/src/pages/MonthlyLadder.jsx | 319 -------------
frontend/src/pages/Profile.css | 90 ----
frontend/src/pages/Profile.jsx | 39 --
frontend/src/utils/api.js | 34 +-
moderation/backend-py/routes/mod_app.py | 1 -
moderation/frontend/src/App.jsx | 1 -
18 files changed, 30 insertions(+), 1401 deletions(-)
delete mode 100644 frontend/src/components/LadderButton.css
delete mode 100644 frontend/src/components/LadderButton.jsx
delete mode 100644 frontend/src/pages/MonthlyLadder.css
delete mode 100644 frontend/src/pages/MonthlyLadder.jsx
diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js
index 48f462d..3b0facc 100644
--- a/backend/middleware/auth.js
+++ b/backend/middleware/auth.js
@@ -172,96 +172,16 @@ const authenticate = async (req, res, next) => {
let user = await User.findOne({ telegramId: normalizedUser.id.toString() });
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({
telegramId: normalizedUser.id.toString(),
username: normalizedUser.username || normalizedUser.firstName || 'user',
firstName: normalizedUser.firstName,
lastName: normalizedUser.lastName,
- photoUrl: normalizedUser.photoUrl,
- referredBy: referredBy
+ photoUrl: normalizedUser.photoUrl
});
await user.save();
-
- if (referredBy) {
- console.log(`✅ Создан новый пользователь ${user.username} (${user._id}) с referredBy: ${referredBy}`);
- } else {
- console.log(`✅ Создан новый пользователь ${user.username} (${user._id}) без реферала`);
- }
-
- // Счетчик рефералов увеличивается только когда пользователь создаст первый пост
- // (см. routes/posts.js)
+ console.log(`✅ Создан новый пользователь ${user.username} (${user._id})`);
} 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) {
user.username = normalizedUser.username;
@@ -295,81 +215,6 @@ const authenticate = async (req, res, next) => {
await ensureUserSettings(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.telegramUser = normalizedUser;
diff --git a/backend/models/TicketActivity.js b/backend/models/TicketActivity.js
index 74b20c4..b483867 100644
--- a/backend/models/TicketActivity.js
+++ b/backend/models/TicketActivity.js
@@ -33,10 +33,6 @@ const TicketActivitySchema = new mongoose.Schema({
type: Number,
default: 0
},
- referralsCounted: {
- type: Number,
- default: 0
- },
// Для отслеживания баллов с реакций на арты (по постам)
// Используем объект вместо Map для совместимости с Mongoose
artReactionsPoints: {
diff --git a/backend/models/User.js b/backend/models/User.js
index 1d09409..bb91af2 100644
--- a/backend/models/User.js
+++ b/backend/models/User.js
@@ -84,29 +84,6 @@ const UserSchema = new mongoose.Schema({
default: false
},
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
tickets: {
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);
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
index 5e2655b..a0154ba 100644
--- a/backend/routes/auth.js
+++ b/backend/routes/auth.js
@@ -57,8 +57,6 @@ const respondWithUser = async (user, res) => {
followingCount: populatedUser.following.length,
followers: populatedUser.followers,
following: populatedUser.following,
- referralCode: populatedUser.referralCode,
- referralsCount: populatedUser.referralsCount || 0,
tickets: populatedUser.tickets || 0,
settings,
banned: populatedUser.banned
diff --git a/backend/routes/modApp.js b/backend/routes/modApp.js
index 61f772c..24546ee 100644
--- a/backend/routes/modApp.js
+++ b/backend/routes/modApp.js
@@ -82,7 +82,6 @@ const serializeUser = (user) => {
bannedUntil: user.bannedUntil,
lastActiveAt: user.lastActiveAt,
createdAt: user.createdAt,
- referralsCount: user.referralsCount || 0,
// passwordHash никогда не возвращается (уже select: false в модели)
// email не возвращается для безопасности
};
diff --git a/backend/routes/users.js b/backend/routes/users.js
index a057887..6e19187 100644
--- a/backend/routes/users.js
+++ b/backend/routes/users.js
@@ -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;
diff --git a/backend/utils/tickets.js b/backend/utils/tickets.js
index 92a93f4..ffdb7ed 100644
--- a/backend/utils/tickets.js
+++ b/backend/utils/tickets.js
@@ -33,9 +33,6 @@ async function canAwardTickets(userId, actionType, options = {}) {
case 'comment_received':
return true; // Нет лимита на получение комментариев
- case 'referral':
- return activity.referralsCounted < 3;
-
case 'art_reaction':
// Проверка лимита на реакцию на арт (100 баллов в сутки с одного арта)
const postId = options.postId;
@@ -108,9 +105,6 @@ async function awardTickets(userId, points, actionType, options = {}) {
case 'comment_received':
activity.commentsReceived += 1;
break;
- case 'referral':
- activity.referralsCounted += 1;
- break;
case 'art_reaction':
const postId = options.postId;
if (postId) {
@@ -176,13 +170,6 @@ async function awardCommentReceived(authorId, 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,
awardCommentWritten,
awardCommentReceived,
- awardReferral,
awardArtLike,
awardArtComment,
awardArtModeration,
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 035d0c2..58ad35c 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -16,7 +16,6 @@ import Profile from './pages/Profile'
import UserProfile from './pages/UserProfile'
import CommentsPage from './pages/CommentsPage'
import PostMenuPage from './pages/PostMenuPage'
-import MonthlyLadder from './pages/MonthlyLadder'
import MiniPlayer from './components/MiniPlayer'
import FullPlayer from './components/FullPlayer'
import './styles/index.css'
@@ -233,7 +232,6 @@ function AppContent() {
} />
} />
} />
- } />
)
diff --git a/frontend/src/components/LadderButton.css b/frontend/src/components/LadderButton.css
deleted file mode 100644
index 5e6d62b..0000000
--- a/frontend/src/components/LadderButton.css
+++ /dev/null
@@ -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);
-}
-
-
-
diff --git a/frontend/src/components/LadderButton.jsx b/frontend/src/components/LadderButton.jsx
deleted file mode 100644
index 2242a97..0000000
--- a/frontend/src/components/LadderButton.jsx
+++ /dev/null
@@ -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 (
-
- )
-}
-
diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx
index 1ffe886..efb18d0 100644
--- a/frontend/src/components/Layout.jsx
+++ b/frontend/src/components/Layout.jsx
@@ -1,6 +1,5 @@
import { Outlet } from 'react-router-dom'
import Navigation from './Navigation'
-import LadderButton from './LadderButton'
import './Layout.css'
export default function Layout({ user }) {
@@ -10,7 +9,6 @@ export default function Layout({ user }) {
-
)
}
diff --git a/frontend/src/pages/MonthlyLadder.css b/frontend/src/pages/MonthlyLadder.css
deleted file mode 100644
index 9064d3d..0000000
--- a/frontend/src/pages/MonthlyLadder.css
+++ /dev/null
@@ -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);
-}
diff --git a/frontend/src/pages/MonthlyLadder.jsx b/frontend/src/pages/MonthlyLadder.jsx
deleted file mode 100644
index 1a83dc8..0000000
--- a/frontend/src/pages/MonthlyLadder.jsx
+++ /dev/null
@@ -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
- case 2:
- return
- case 3:
- return
- default:
- return {rank}
- }
- }
-
- 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 (
-
- {/* Хедер */}
-
-
-
Monthly Ladder
-
-
-
- {/* Новогодний декор */}
-
-
❄️
-
❄️
-
❄️
-
❄️
-
❄️
-
❄️
-
-
- {/* Отсчет до нового года */}
-
-
-
-
До Нового Года
-
-
-
- {timeLeft.days}
- дней
-
-
:
-
- {String(timeLeft.hours).padStart(2, '0')}
- часов
-
-
:
-
- {String(timeLeft.minutes).padStart(2, '0')}
- минут
-
-
:
-
- {String(timeLeft.seconds).padStart(2, '0')}
- секунд
-
-
-
Ваши посты, ваши арты, ваша слава. Остальное потом.
-
-
- {/* Топ 5 пользователей */}
-
-
-
Топ 5
-
-
-
- {loading ? (
-
- ) : (
-
- {topUsers.map((topUser, index) => {
- const isCurrentUser = user && (topUser._id === user.id || topUser._id?.toString() === user.id?.toString())
- const prize = getPrize(topUser.rank)
- return (
-
-
- {getRankIcon(topUser.rank)}
-
-

-
-
- {topUser.firstName || topUser.username}
- {isCurrentUser && }
-
-
-
- {formatTickets(topUser.tickets)} {getTicketsWord(topUser.tickets)}
- {prize && {prize}}
-
-
- )
- })}
-
- )}
-
-
- {/* Текущий пользователь (если не в топе) */}
- {currentUser && currentUserRank > 5 && (
-
-
Ваша позиция
-
-
- {currentUserRank}
-
-

-
-
- {currentUser.firstName || currentUser.username}
-
-
-
-
- {formatTickets(currentUser.tickets)} {getTicketsWord(currentUser.tickets)}
-
-
-
- )}
-
- {/* Модальное окно с информацией */}
- {showInfo && (
-
setShowInfo(false)}>
-
e.stopPropagation()}>
-
-
За что начисляются билеты
-
-
-
-
-
1. Посты
-
+15 билетов за создание поста
-
Лимит: 5 постов в день
-
-
-
-
2. Лайки
-
Ставишь лайки: +1 билет за лайк
-
Лимит: 50 в день
-
Получаешь лайки: +2 билета за лайк под твоей записью
-
Лимит учёта: 100 лайков в день
-
-
-
-
3. Комментарии
-
Пишешь комментарии: +4 билета за комментарий длиной 10+ символов
-
Лимит: 20 комментариев в день
-
Получаешь комментарии: +6 билетов за комментарий под твоим постом
-
-
-
-
4. Рефералы
-
+100 билетов за одного валидного реферала
-
Лимит: 3 реферала в день
-
-
-
-
5. Ваше творчество (арты)
-
Публикация: +40 билетов за арт, прошедший модерацию
-
Лимит: 1 арт в день / 5 в неделю
-
Реакции на арт:
-
+8 билетов за лайк под артом
-
+12 билетов за комментарий под артом (1 комментарий от одного человека в сутки)
-
Лимит: до 100 билетов в сутки с реакций на один арт
-
-
-
-
Немного правил
-
Лайки/комменты от аккаунтов младше 24 часов не считаем
-
Комменты <10 символов = 0 билетов
-
Ограничение на билеты по входящим реакциям, чтобы боты не устроили ферму
-
-
-
-
- )}
-
- )
-}
-
diff --git a/frontend/src/pages/Profile.css b/frontend/src/pages/Profile.css
index ffffdb2..1e0f0e7 100644
--- a/frontend/src/pages/Profile.css
+++ b/frontend/src/pages/Profile.css
@@ -184,96 +184,6 @@
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 {
display: flex;
background: var(--bg-primary);
diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx
index a472341..6c090e9 100644
--- a/frontend/src/pages/Profile.jsx
+++ b/frontend/src/pages/Profile.jsx
@@ -318,18 +318,6 @@ export default function Profile({ user, setUser }) {
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 (
@@ -405,33 +393,6 @@ export default function Profile({ user, setUser }) {
- {/* Реферальная карточка */}
- {user.referralCode && (
-
-
-
-
-
-
-
Реферальная программа
-
Приглашайте друзей и получайте бонусы за каждого приглашенного пользователя!
-
- Приглашено: {user.referralsCount || 0}
-
-
-
-
-
- {`https://t.me/${import.meta.env.VITE_BOT_USERNAME || 'NakamaSpaceBot'}?startapp=ref_${user.referralCode}`}
-
-
-
-
- )}
-
{/* Привязка email (только для Telegram пользователей без email) */}
{user.telegramId && !user.email && (
diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js
index ca92831..6701f37 100644
--- a/frontend/src/utils/api.js
+++ b/frontend/src/utils/api.js
@@ -113,6 +113,34 @@ export const verifyAuth = async () => {
}
// Авторизация через 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
export const getPosts = async (params = {}) => {
// Поддержка старого формата для обратной совместимости
@@ -205,12 +233,6 @@ export const searchUsers = async (query) => {
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
export const getNotifications = async (params = {}) => {
const response = await api.get('/notifications', { params })
diff --git a/moderation/backend-py/routes/mod_app.py b/moderation/backend-py/routes/mod_app.py
index dff114a..bb3c490 100644
--- a/moderation/backend-py/routes/mod_app.py
+++ b/moderation/backend-py/routes/mod_app.py
@@ -114,7 +114,6 @@ async def get_users(
'bannedUntil': banned_until,
'createdAt': created_at,
'lastActiveAt': last_active_at,
- 'referralsCount': int(u.get('referralsCount', 0)),
'isAdmin': is_admin
})
diff --git a/moderation/frontend/src/App.jsx b/moderation/frontend/src/App.jsx
index 1a0ddd1..c905504 100644
--- a/moderation/frontend/src/App.jsx
+++ b/moderation/frontend/src/App.jsx
@@ -1009,7 +1009,6 @@ export default function App() {
Роль: {u.role}
Активность: {formatDate(u.lastActiveAt)}
- {u.referralsCount > 0 && Рефералов: {u.referralsCount}}
{u.banned && Бан до {formatDate(u.bannedUntil)}}