✨ Реферальная система: БД, API, UI
**База данных:** - Создана таблица referrals в MariaDB (193.168.175.128) - Поля: username, referrer_username, referral_count, bonus_days_earned - Foreign keys к users таблице с CASCADE/SET NULL **API Endpoints:** - POST /api/referral/track - отслеживание новых рефералов - Автоматический расчёт бонусов: +7 дней за каждого - Milestone bonus: +30 дней за каждые 5 рефералов - Обновление expire даты реферера в users таблице - GET /api/referral/stats?username=xxx - статистика - Возвращает количество рефералов, бонусные дни - Список приглашённых пользователей со статусами **Интеграция:** - POST /api/create-user принимает referrerId параметр - Автоматический вызов /api/referral/track после создания юзера - Параметр ref из URL при активации trial **UI:** - /app/referral/page.tsx - страница статистики - 3 KPI карточки: рефералов, бонусных дней, milestone - Реферальная ссылка с кнопкой копирования - Список приглашённых юзеров с иконками статуса - Инфоблок о механике начисления бонусов **ReferralModal обновлён:** - Добавлена кнопка «Моя статистика» → /referral - Перенос Share/Copy кнопок на второй/третий план **Зависимости:** - mysql2@3.16.3 - для подключения к MariaDB **Логика бонусов:** - +7 дней за каждого успешного реферала - +30 дней бонус за каждые 5 рефералов (milestone) - Автоматическое обновление expire поля в users таблице - Сохранение всех бонусов в bonus_days_earned
This commit is contained in:
@@ -19,9 +19,9 @@ if (!ADMIN_USERNAME || !ADMIN_PASSWORD) {
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { planType, telegramId, telegramUsername, firstName, lastName } = await request.json();
|
||||
const { planType, telegramId, telegramUsername, firstName, lastName, referrerId } = await request.json();
|
||||
|
||||
logger.debug('📥 Received data:', { planType, telegramId, telegramUsername, firstName, lastName });
|
||||
logger.debug('📥 Received data:', { planType, telegramId, telegramUsername, firstName, lastName, referrerId });
|
||||
|
||||
// 1. Получаем токен админа
|
||||
const tokenResponse = await fetch(`${MARZBAN_API}/api/admin/token`, {
|
||||
@@ -101,6 +101,24 @@ export async function POST(request: NextRequest) {
|
||||
// 5. Получаем subscription token
|
||||
const subscriptionToken = userData.subscription_url?.split('/sub/')[1]?.replace(/\/$/, '') || username;
|
||||
|
||||
// 6. Track referral if referrerId provided
|
||||
if (referrerId) {
|
||||
try {
|
||||
await fetch(`${request.nextUrl.origin}/api/referral/track`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
referrer_username: referrerId,
|
||||
}),
|
||||
});
|
||||
logger.debug('✅ Referral tracked:', { username, referrer: referrerId });
|
||||
} catch (refError) {
|
||||
logger.error('❌ Referral tracking failed:', refError);
|
||||
// Don't fail user creation if referral tracking fails
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
token: subscriptionToken,
|
||||
|
||||
90
app/api/referral/stats/route.ts
Normal file
90
app/api/referral/stats/route.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// API endpoint для получения статистики реферальной программы
|
||||
// GET /api/referral/stats?username=xxx - возвращает статистику пользователя
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
// Database connection config
|
||||
const dbConfig = {
|
||||
host: '193.168.175.128',
|
||||
user: 'marzban_user',
|
||||
password: '2CuopqFd0Y5V5n/qBM+eygOQb6aC8B8pACcdHjeVJsE=',
|
||||
database: 'marzban_prod',
|
||||
};
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const username = searchParams.get('username');
|
||||
|
||||
if (!username) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Username is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
const connection = await mysql.createConnection(dbConfig);
|
||||
|
||||
try {
|
||||
// Get user's referral stats
|
||||
const [statsRows] = await connection.query(
|
||||
'SELECT * FROM referrals WHERE username = ?',
|
||||
[username]
|
||||
);
|
||||
|
||||
if ((statsRows as any[]).length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
hasReferrals: false,
|
||||
stats: {
|
||||
referral_count: 0,
|
||||
bonus_days_earned: 0,
|
||||
total_referrals_used: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const stats = (statsRows as any[])[0];
|
||||
|
||||
// Get list of referred users
|
||||
const [referredRows] = await connection.query(
|
||||
`SELECT u.username, u.created_at, u.status
|
||||
FROM referrals r
|
||||
JOIN users u ON r.username = u.username
|
||||
WHERE r.referrer_username = ?
|
||||
ORDER BY r.created_at DESC`,
|
||||
[username]
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
hasReferrals: true,
|
||||
stats: {
|
||||
referral_count: stats.referral_count || 0,
|
||||
bonus_days_earned: stats.bonus_days_earned || 0,
|
||||
total_referrals_used: stats.total_referrals_used || 0,
|
||||
created_at: stats.created_at,
|
||||
updated_at: stats.updated_at,
|
||||
},
|
||||
referred_users: (referredRows as any[]).map((user: any) => ({
|
||||
username: user.username,
|
||||
created_at: user.created_at,
|
||||
status: user.status,
|
||||
})),
|
||||
});
|
||||
} finally {
|
||||
await connection.end();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Referral stats error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
135
app/api/referral/track/route.ts
Normal file
135
app/api/referral/track/route.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
// API endpoint для отслеживания реферальной регистрации
|
||||
// POST /api/referral/track - записывает нового пользователя с реферером
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import mysql from 'mysql2/promise';
|
||||
|
||||
// Database connection config
|
||||
const dbConfig = {
|
||||
host: '193.168.175.128',
|
||||
user: 'marzban_user',
|
||||
password: '2CuopqFd0Y5V5n/qBM+eygOQb6aC8B8pACcdHjeVJsE=',
|
||||
database: 'marzban_prod',
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { username, referrer_username } = body;
|
||||
|
||||
if (!username) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Username is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
const connection = await mysql.createConnection(dbConfig);
|
||||
|
||||
try {
|
||||
// Check if user already exists in referrals table
|
||||
const [existingRows] = await connection.query(
|
||||
'SELECT id FROM referrals WHERE username = ?',
|
||||
[username]
|
||||
);
|
||||
|
||||
if ((existingRows as any[]).length > 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'User already tracked',
|
||||
});
|
||||
}
|
||||
|
||||
// Insert new referral record
|
||||
await connection.query(
|
||||
'INSERT INTO referrals (username, referrer_username) VALUES (?, ?)',
|
||||
[username, referrer_username || null]
|
||||
);
|
||||
|
||||
// If referrer exists, increment their referral_count
|
||||
if (referrer_username) {
|
||||
// Check if referrer exists in users table
|
||||
const [referrerRows] = await connection.query(
|
||||
'SELECT username FROM users WHERE username = ?',
|
||||
[referrer_username]
|
||||
);
|
||||
|
||||
if ((referrerRows as any[]).length > 0) {
|
||||
// Update or insert referrer's stats
|
||||
await connection.query(
|
||||
`INSERT INTO referrals (username, referral_count)
|
||||
VALUES (?, 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
referral_count = referral_count + 1,
|
||||
updated_at = CURRENT_TIMESTAMP`,
|
||||
[referrer_username]
|
||||
);
|
||||
|
||||
// Calculate bonus days
|
||||
const [statsRows] = await connection.query(
|
||||
'SELECT referral_count FROM referrals WHERE username = ?',
|
||||
[referrer_username]
|
||||
);
|
||||
|
||||
const referralCount = (statsRows as any[])[0]?.referral_count || 0;
|
||||
let bonusDays = 0;
|
||||
|
||||
// +7 days for each referral
|
||||
bonusDays = referralCount * 7;
|
||||
|
||||
// +30 days milestone bonus for every 5 referrals
|
||||
if (referralCount >= 5) {
|
||||
const milestones = Math.floor(referralCount / 5);
|
||||
bonusDays += milestones * 30;
|
||||
}
|
||||
|
||||
// Update bonus_days_earned
|
||||
await connection.query(
|
||||
'UPDATE referrals SET bonus_days_earned = ? WHERE username = ?',
|
||||
[bonusDays, referrer_username]
|
||||
);
|
||||
|
||||
// Add bonus days to referrer's expire date in users table
|
||||
await connection.query(
|
||||
`UPDATE users
|
||||
SET expire = CASE
|
||||
WHEN expire IS NULL OR expire < UNIX_TIMESTAMP()
|
||||
THEN UNIX_TIMESTAMP() + (? * 86400)
|
||||
ELSE expire + (7 * 86400)
|
||||
END
|
||||
WHERE username = ?`,
|
||||
[bonusDays, referrer_username]
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Referral tracked successfully',
|
||||
referrer_bonus: {
|
||||
username: referrer_username,
|
||||
new_referral_count: referralCount,
|
||||
bonus_days_added: 7,
|
||||
total_bonus_days: bonusDays,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'User tracked without referrer',
|
||||
});
|
||||
} finally {
|
||||
await connection.end();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Referral track error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -175,6 +175,10 @@ export default function Home() {
|
||||
const handleActivateTrial = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Получаем referrerId из URL (если есть)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const referrerId = urlParams.get('ref');
|
||||
|
||||
// Создаем trial подписку через API
|
||||
const response = await fetch('/api/create-user', {
|
||||
method: 'POST',
|
||||
@@ -184,6 +188,7 @@ export default function Home() {
|
||||
telegramId: Date.now(), // Временно, пока нет настоящего Telegram ID
|
||||
telegramUsername: 'demo_user',
|
||||
firstName: 'Demo',
|
||||
referrerId: referrerId || undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
296
app/referral/page.tsx
Normal file
296
app/referral/page.tsx
Normal file
@@ -0,0 +1,296 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Shield,
|
||||
ArrowLeft,
|
||||
Users,
|
||||
Gift,
|
||||
TrendingUp,
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
Copy,
|
||||
Share2
|
||||
} from 'lucide-react';
|
||||
|
||||
interface ReferralStats {
|
||||
referral_count: number;
|
||||
bonus_days_earned: number;
|
||||
total_referrals_used: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface ReferredUser {
|
||||
username: string;
|
||||
created_at: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export default function ReferralPage() {
|
||||
const router = useRouter();
|
||||
const [stats, setStats] = useState<ReferralStats | null>(null);
|
||||
const [referredUsers, setReferredUsers] = useState<ReferredUser[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [username, setUsername] = useState<string>('');
|
||||
const [referralUrl, setReferralUrl] = useState<string>('');
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [toastMessage, setToastMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const loadReferralStats = async () => {
|
||||
// Get username from localStorage
|
||||
const token = localStorage.getItem('subscriptionToken');
|
||||
if (!token) {
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = token.split('_')[0];
|
||||
setUsername(userId);
|
||||
|
||||
// Generate referral URL
|
||||
const botUsername = process.env.NEXT_PUBLIC_TELEGRAM_BOT_USERNAME || 'Dorod_vps_bot';
|
||||
const url = `https://t.me/${botUsername}?start=ref_${userId}`;
|
||||
setReferralUrl(url);
|
||||
|
||||
// Fetch stats from API
|
||||
try {
|
||||
const response = await fetch(`/api/referral/stats?username=${userId}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.hasReferrals) {
|
||||
setStats(data.stats);
|
||||
setReferredUsers(data.referred_users || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load referral stats:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadReferralStats();
|
||||
}, [router]);
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
showToastNotification('✅ Скопировано в буфер обмена!');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
showToastNotification('❌ Ошибка копирования');
|
||||
}
|
||||
};
|
||||
|
||||
const showToastNotification = (message: string) => {
|
||||
setToastMessage(message);
|
||||
setShowToast(true);
|
||||
setTimeout(() => setShowToast(false), 3000);
|
||||
};
|
||||
|
||||
const shareReferralLink = async () => {
|
||||
const shareText = `🚀 Попробуй Umbrix VPN - быстрый и безопасный VPN!\n\n✨ Получи 7 дней бесплатно по моей ссылке:\n${referralUrl}`;
|
||||
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: 'Umbrix VPN - Пригласи друга!',
|
||||
text: shareText,
|
||||
url: referralUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
if ((err as Error).name !== 'AbortError') {
|
||||
console.error('Share failed:', err);
|
||||
copyToClipboard(shareText);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
copyToClipboard(shareText);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
||||
case 'expired':
|
||||
return <Clock className="w-4 h-4 text-yellow-500" />;
|
||||
case 'disabled':
|
||||
return <XCircle className="w-4 h-4 text-red-500" />;
|
||||
default:
|
||||
return <Clock className="w-4 h-4 text-slate-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'Активен';
|
||||
case 'expired':
|
||||
return 'Истёк';
|
||||
case 'disabled':
|
||||
return 'Отключён';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center" style={{ background: 'var(--bg-app)' }}>
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 mx-auto" style={{ borderColor: 'var(--primary)' }}></div>
|
||||
<p className="mt-4 text-slate-400">Загрузка...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen" style={{ background: 'var(--bg-app)', color: 'var(--text-white)' }}>
|
||||
{/* Header */}
|
||||
<header className="flex items-center justify-between p-4 border-b sticky top-0 z-40 backdrop-blur-sm" style={{ borderColor: 'var(--border)', background: 'var(--bg-card)' }}>
|
||||
<button onClick={() => router.back()} className="p-2 rounded-lg hover:opacity-70 transition-opacity" style={{ background: 'var(--bg-elevated)' }}>
|
||||
<ArrowLeft className="w-5 h-5" style={{ color: 'var(--text-primary)' }} />
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-6 h-6" style={{ color: 'var(--primary)' }} />
|
||||
<span className="text-xl font-bold">Реферальная программа</span>
|
||||
</div>
|
||||
<div className="w-9" /> {/* Spacer */}
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="px-6 py-8 max-w-2xl mx-auto">
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-3 gap-3 mb-6">
|
||||
<div className="rounded-xl p-4 text-center" style={{ background: 'var(--bg-card)' }}>
|
||||
<Users className="w-6 h-6 mx-auto mb-2" style={{ color: 'var(--primary)' }} />
|
||||
<div className="text-2xl font-bold mb-1">{stats?.referral_count || 0}</div>
|
||||
<div className="text-xs text-slate-400">Рефералов</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl p-4 text-center" style={{ background: 'var(--bg-card)' }}>
|
||||
<Gift className="w-6 h-6 mx-auto mb-2" style={{ color: 'var(--primary)' }} />
|
||||
<div className="text-2xl font-bold mb-1">{stats?.bonus_days_earned || 0}</div>
|
||||
<div className="text-xs text-slate-400">Дней бонуса</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl p-4 text-center" style={{ background: 'var(--bg-card)' }}>
|
||||
<TrendingUp className="w-6 h-6 mx-auto mb-2" style={{ color: 'var(--primary)' }} />
|
||||
<div className="text-2xl font-bold mb-1">+{(stats?.referral_count || 0) >= 5 ? 30 : 0}</div>
|
||||
<div className="text-xs text-slate-400">Milestone</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Referral Link */}
|
||||
<div className="rounded-xl p-4 mb-6" style={{ background: 'var(--bg-card)' }}>
|
||||
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
||||
<Gift className="w-4 h-4" style={{ color: 'var(--primary)' }} />
|
||||
Ваша реферальная ссылка
|
||||
</h3>
|
||||
|
||||
<div className="flex gap-2 mb-3">
|
||||
<input
|
||||
type="text"
|
||||
value={referralUrl}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 rounded-lg text-sm"
|
||||
style={{ background: 'var(--bg-elevated)', color: 'var(--text-white)', border: '1px solid var(--border)' }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => copyToClipboard(referralUrl)}
|
||||
className="p-2 rounded-lg hover:opacity-80 transition-opacity"
|
||||
style={{ background: 'var(--bg-elevated)' }}
|
||||
>
|
||||
<Copy className="w-5 h-5" style={{ color: 'var(--primary)' }} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={shareReferralLink}
|
||||
className="w-full flex items-center justify-center gap-2 py-3 rounded-lg font-medium hover:opacity-80 transition-opacity"
|
||||
style={{ background: 'var(--primary)', color: 'white' }}
|
||||
>
|
||||
<Share2 className="w-5 h-5" />
|
||||
Поделиться ссылкой
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Bonus Info */}
|
||||
<div className="rounded-xl p-4 mb-6" style={{ background: 'var(--bg-card)', border: '1px solid var(--border)' }}>
|
||||
<h3 className="text-sm font-semibold mb-3">💰 Как работают бонусы</h3>
|
||||
<ul className="space-y-2 text-sm text-slate-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="w-4 h-4 mt-0.5 flex-shrink-0" style={{ color: 'var(--primary)' }} />
|
||||
<span>+7 дней за каждого приглашённого друга</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="w-4 h-4 mt-0.5 flex-shrink-0" style={{ color: 'var(--primary)' }} />
|
||||
<span>+30 дней бонус за каждые 5 рефералов</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="w-4 h-4 mt-0.5 flex-shrink-0" style={{ color: 'var(--primary)' }} />
|
||||
<span>Бонусы начисляются автоматически</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Referred Users */}
|
||||
{referredUsers.length > 0 && (
|
||||
<div className="rounded-xl p-4" style={{ background: 'var(--bg-card)' }}>
|
||||
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
||||
<Users className="w-4 h-4" style={{ color: 'var(--primary)' }} />
|
||||
Ваши рефералы ({referredUsers.length})
|
||||
</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
{referredUsers.map((user, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-3 rounded-lg"
|
||||
style={{ background: 'var(--bg-elevated)' }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{getStatusIcon(user.status)}
|
||||
<div>
|
||||
<div className="text-sm font-medium">{user.username}</div>
|
||||
<div className="text-xs text-slate-400">
|
||||
{new Date(user.created_at).toLocaleDateString('ru-RU')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs px-2 py-1 rounded" style={{ background: 'var(--bg-card)' }}>
|
||||
{getStatusText(user.status)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{referredUsers.length === 0 && (
|
||||
<div className="text-center py-12 rounded-xl" style={{ background: 'var(--bg-card)' }}>
|
||||
<Users className="w-16 h-16 mx-auto mb-4 opacity-50" style={{ color: 'var(--primary)' }} />
|
||||
<p className="text-slate-400 mb-2">Пока нет рефералов</p>
|
||||
<p className="text-sm text-slate-500">Поделитесь ссылкой с друзьями!</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Toast Notification */}
|
||||
{showToast && (
|
||||
<div className="fixed bottom-24 left-1/2 transform -translate-x-1/2 z-50 animate-fade-in">
|
||||
<div className="px-6 py-3 rounded-full shadow-lg border flex items-center gap-2" style={{ background: 'var(--bg-card)', borderColor: 'var(--border)' }}>
|
||||
<span>{toastMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { X, Copy, Share2, Gift, Users, Award } from 'lucide-react';
|
||||
import { X, Copy, Share2, Gift, Users, Award, BarChart3 } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface ReferralModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -105,10 +106,20 @@ export default function ReferralModal({
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="space-y-3">
|
||||
<Link href="/referral">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full flex items-center justify-center gap-3 px-6 py-4 rounded-xl font-semibold transition-all shadow-lg hover:opacity-90"
|
||||
style={{ background: 'var(--primary)', color: 'white' }}
|
||||
>
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
Моя статистика
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={onShare}
|
||||
className="w-full flex items-center justify-center gap-3 px-6 py-4 rounded-xl font-semibold transition-all shadow-lg hover:opacity-90"
|
||||
style={{ background: 'var(--primary)', color: 'white' }}
|
||||
className="w-full flex items-center justify-center gap-3 px-6 py-4 bg-slate-700 hover:bg-slate-600 rounded-xl font-semibold transition-all"
|
||||
>
|
||||
<Share2 className="w-5 h-5" />
|
||||
Поделиться с друзьями
|
||||
|
||||
123
package-lock.json
generated
123
package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"eslint": "8.49.0",
|
||||
"eslint-config-next": "13.5.1",
|
||||
"lucide-react": "^0.563.0",
|
||||
"mysql2": "^3.16.3",
|
||||
"next": "13.5.1",
|
||||
"postcss": "8.4.30",
|
||||
"qrcode": "^1.5.4",
|
||||
@@ -882,6 +883,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axe-core": {
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz",
|
||||
@@ -1341,6 +1351,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
@@ -2184,6 +2203,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
@@ -2417,6 +2445,22 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -2722,6 +2766,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-property": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
|
||||
@@ -3012,6 +3062,12 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -3028,6 +3084,21 @@
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
|
||||
},
|
||||
"node_modules/lru.min": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
|
||||
"integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=1.30.0",
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wellwelwel"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.563.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz",
|
||||
@@ -3089,6 +3160,26 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.16.3",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.3.tgz",
|
||||
"integrity": "sha512-+3XhQEt4FEFuvGV0JjIDj4eP2OT/oIj/54dYvqhblnSzlfcxVOuj+cd15Xz6hsG4HU1a+A5+BA9gm0618C4z7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aws-ssl-profiles": "^1.1.2",
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"long": "^5.3.2",
|
||||
"lru.min": "^1.1.3",
|
||||
"named-placeholders": "^1.1.6",
|
||||
"seq-queue": "^0.0.5",
|
||||
"sqlstring": "^2.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
@@ -3099,6 +3190,18 @@
|
||||
"thenify-all": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/named-placeholders": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
|
||||
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lru.min": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
@@ -3934,6 +4037,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
@@ -3953,6 +4062,11 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/seq-queue": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
|
||||
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
@@ -4052,6 +4166,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sqlstring": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
||||
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/stop-iteration-iterator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"eslint": "8.49.0",
|
||||
"eslint-config-next": "13.5.1",
|
||||
"lucide-react": "^0.563.0",
|
||||
"mysql2": "^3.16.3",
|
||||
"next": "13.5.1",
|
||||
"postcss": "8.4.30",
|
||||
"qrcode": "^1.5.4",
|
||||
|
||||
Reference in New Issue
Block a user