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

Ваша позиция

-
-
- {currentUserRank} -
- {currentUser.username} -
-
- {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)}}