Files
app_umbrix/app/referral/page.tsx
Umbrix Dev b43eb3c724 Реферальная система: БД, 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
2026-02-06 20:51:40 +03:00

297 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}