Безопасность: - proxy: белый список путей (только /sub/*), POST заблокирован - console.log заменён на logger (утечки URL/данных) - OnboardingFlow: убраны --tg-theme-* (не существуют в проекте) TypeScript (0 ошибок): - tsconfig target es5→es2017 (regex /u flag fix) - layout.tsx: viewport перенесён в metadata (Next.js 13.5) - telegram-webhook: fix text possibly undefined - hooks/useTelegramWebApp: fix Object possibly undefined - types/telegram: убрана дублирующая Window декларация UI: - BottomNav: новый компонент (Назад/Главная/Помощь) - safe-area-bottom CSS класс добавлен в globals.css - dashboard: spacer h-20, toast поднят над BottomNav - OnboardingFlow: цены 149/249/350₽ (были 200/350/500₽) Очистка: - page_NEW.tsx удалён локально (не был в git)
305 lines
12 KiB
TypeScript
305 lines
12 KiB
TypeScript
// app/dashboard/page.tsx - Main dashboard for existing users
|
||
// Shows subscription status, QR code, referral stats, quick actions
|
||
|
||
'use client';
|
||
|
||
import { useState, useEffect } from 'react';
|
||
import { useRouter } from 'next/navigation';
|
||
import Link from 'next/link';
|
||
import QRCodeModal from '@/components/QRCodeModal';
|
||
import ReferralModal from '@/components/ReferralModal';
|
||
import { marzbanApi } from '@/lib/marzban-api';
|
||
import { getSubscriptionUrl } from '@/lib/constants';
|
||
import {
|
||
Shield,
|
||
Settings,
|
||
Gift,
|
||
HelpCircle,
|
||
QrCode,
|
||
Copy,
|
||
Share2,
|
||
CheckCircle,
|
||
AlertCircle,
|
||
Clock,
|
||
Users,
|
||
} from 'lucide-react';
|
||
|
||
export default function Dashboard() {
|
||
const router = useRouter();
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [subscriptionToken, setSubscriptionToken] = useState<string | null>(null);
|
||
const [subscriptionStatus, setSubscriptionStatus] = useState<'active' | 'expired' | 'trial'>('active');
|
||
const [expiryDate, setExpiryDate] = useState<string>('');
|
||
const [daysRemaining, setDaysRemaining] = useState<number>(0);
|
||
const [username, setUsername] = useState<string>('');
|
||
const [isQROpen, setIsQROpen] = useState(false);
|
||
const [isReferralOpen, setIsReferralOpen] = useState(false);
|
||
const [showToast, setShowToast] = useState(false);
|
||
const [toastMessage, setToastMessage] = useState('');
|
||
const [referralCount, setReferralCount] = useState(0);
|
||
const [bonusDays, setBonusDays] = useState(0);
|
||
|
||
const subscriptionUrl = subscriptionToken ? getSubscriptionUrl(subscriptionToken) : '';
|
||
|
||
useEffect(() => {
|
||
loadUserData();
|
||
}, []);
|
||
|
||
const loadUserData = async () => {
|
||
try {
|
||
// Get Telegram data
|
||
const telegramWebApp = (window as any).Telegram?.WebApp;
|
||
const telegramId = telegramWebApp?.initDataUnsafe?.user?.id;
|
||
const telegramUsername = telegramWebApp?.initDataUnsafe?.user?.username;
|
||
|
||
if (!telegramId && !telegramUsername) {
|
||
console.log('❌ No Telegram data - redirecting to home');
|
||
router.push('/');
|
||
return;
|
||
}
|
||
|
||
// Load subscription
|
||
const params = new URLSearchParams();
|
||
if (telegramId) params.append('telegramId', telegramId.toString());
|
||
if (telegramUsername) params.append('telegramUsername', telegramUsername);
|
||
|
||
const response = await fetch(`/api/user-subscription?${params.toString()}`);
|
||
const data = await response.json();
|
||
|
||
if (!data.success || !data.hasSubscription) {
|
||
console.log('❌ No subscription - redirecting to home');
|
||
router.push('/');
|
||
return;
|
||
}
|
||
|
||
setSubscriptionToken(data.token);
|
||
setUsername(data.username);
|
||
setSubscriptionStatus(data.status === 'active' ? 'active' : 'expired');
|
||
|
||
if (data.expire) {
|
||
setExpiryDate(marzbanApi.formatExpireDate(data.expire));
|
||
|
||
// Calculate days remaining
|
||
const expireTimestamp = data.expire * 1000;
|
||
const now = Date.now();
|
||
const diff = expireTimestamp - now;
|
||
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
||
setDaysRemaining(days > 0 ? days : 0);
|
||
}
|
||
|
||
// Load referral stats
|
||
const referralResponse = await fetch(`/api/referral/stats?username=${data.username}`);
|
||
const referralData = await referralResponse.json();
|
||
|
||
if (referralData.success) {
|
||
setReferralCount(referralData.referral_count || 0);
|
||
setBonusDays(referralData.bonus_days_earned || 0);
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load user data:', error);
|
||
router.push('/');
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const showToastNotification = (message: string) => {
|
||
setToastMessage(message);
|
||
setShowToast(true);
|
||
setTimeout(() => setShowToast(false), 3000);
|
||
};
|
||
|
||
const copyToClipboard = async (text: string) => {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
showToastNotification('✅ Скопировано в буфер обмена!');
|
||
} catch (err) {
|
||
console.error('Failed to copy:', err);
|
||
showToastNotification('❌ Ошибка копирования');
|
||
}
|
||
};
|
||
|
||
const shareReferralLink = async () => {
|
||
const botUsername = process.env.NEXT_PUBLIC_TELEGRAM_BOT_USERNAME || 'Dorod_vps_bot';
|
||
const referralUrl = `https://t.me/${botUsername}?start=ref_${username}`;
|
||
|
||
const telegramWebApp = (window as any).Telegram?.WebApp;
|
||
if (telegramWebApp?.openTelegramLink) {
|
||
const shareText = encodeURIComponent(`🚀 Попробуй Umbrix VPN! Получи 7 дней бесплатно по моей ссылке:\n${referralUrl}`);
|
||
telegramWebApp.openTelegramLink(`https://t.me/share/url?url=${referralUrl}&text=${shareText}`);
|
||
} else {
|
||
copyToClipboard(referralUrl);
|
||
}
|
||
};
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 text-white flex items-center justify-center">
|
||
<div className="text-center">
|
||
<div className="animate-spin rounded-full h-16 w-16 border-b-4 border-blue-500 mx-auto mb-6"></div>
|
||
<p className="text-slate-400">Загрузка...</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 text-white">
|
||
{/* Header */}
|
||
<header className="border-b border-slate-700 bg-slate-900/50 backdrop-blur-sm sticky top-0 z-50">
|
||
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
|
||
<h1 className="text-xl font-bold">🚀 Umbrix VPN</h1>
|
||
<Link href="/help">
|
||
<button className="p-2 hover:bg-slate-700 rounded-lg transition-colors">
|
||
<HelpCircle className="h-6 w-6" />
|
||
</button>
|
||
</Link>
|
||
</div>
|
||
</header>
|
||
|
||
{/* Main Content */}
|
||
<main className="max-w-7xl mx-auto px-4 py-8 space-y-6">
|
||
{/* Subscription Status Card */}
|
||
<div className="bg-slate-800/50 border border-slate-700 rounded-lg p-6">
|
||
<div className="flex items-start justify-between mb-4">
|
||
<div>
|
||
<h2 className="text-2xl font-bold mb-2">Ваша подписка</h2>
|
||
<p className="text-slate-400">@{username}</p>
|
||
</div>
|
||
{subscriptionStatus === 'active' ? (
|
||
<div className="flex items-center gap-2 px-3 py-1 bg-green-600/20 border border-green-500 rounded-full">
|
||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||
<span className="text-sm font-medium">Активна</span>
|
||
</div>
|
||
) : (
|
||
<div className="flex items-center gap-2 px-3 py-1 bg-red-600/20 border border-red-500 rounded-full">
|
||
<AlertCircle className="h-4 w-4 text-red-400" />
|
||
<span className="text-sm font-medium">Истекла</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="bg-slate-900/50 rounded-lg p-4">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<Clock className="h-5 w-5 text-blue-400" />
|
||
<span className="text-sm text-slate-400">Осталось дней</span>
|
||
</div>
|
||
<p className="text-3xl font-bold">{daysRemaining}</p>
|
||
<p className="text-sm text-slate-400 mt-1">{expiryDate}</p>
|
||
</div>
|
||
|
||
<div className="bg-slate-900/50 rounded-lg p-4">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<Users className="h-5 w-5 text-purple-400" />
|
||
<span className="text-sm text-slate-400">Рефералы</span>
|
||
</div>
|
||
<p className="text-3xl font-bold">{referralCount}</p>
|
||
<p className="text-sm text-slate-400 mt-1">+{bonusDays} дней заработано</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quick Actions */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<button
|
||
onClick={() => setIsQROpen(true)}
|
||
className="bg-slate-800/50 hover:bg-slate-700 border border-slate-700 rounded-lg p-6 flex flex-col items-center gap-3 transition-colors"
|
||
>
|
||
<QrCode className="h-8 w-8 text-blue-500" />
|
||
<span className="font-semibold">QR Код</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => copyToClipboard(subscriptionUrl)}
|
||
className="bg-slate-800/50 hover:bg-slate-700 border border-slate-700 rounded-lg p-6 flex flex-col items-center gap-3 transition-colors"
|
||
>
|
||
<Copy className="h-8 w-8 text-green-500" />
|
||
<span className="font-semibold">Копировать ссылку</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={shareReferralLink}
|
||
className="bg-slate-800/50 hover:bg-slate-700 border border-slate-700 rounded-lg p-6 flex flex-col items-center gap-3 transition-colors"
|
||
>
|
||
<Share2 className="h-8 w-8 text-purple-500" />
|
||
<span className="font-semibold">Пригласить друга</span>
|
||
</button>
|
||
|
||
<Link href="/referral">
|
||
<button className="w-full bg-slate-800/50 hover:bg-slate-700 border border-slate-700 rounded-lg p-6 flex flex-col items-center gap-3 transition-colors">
|
||
<Gift className="h-8 w-8 text-yellow-500" />
|
||
<span className="font-semibold">Реферальная программа</span>
|
||
</button>
|
||
</Link>
|
||
</div>
|
||
|
||
{/* Referral Progress (if has referrals) */}
|
||
{referralCount > 0 && (
|
||
<div className="bg-gradient-to-r from-purple-600/20 to-blue-600/20 border border-purple-500/50 rounded-lg p-6">
|
||
<h3 className="text-lg font-bold mb-4">🎉 Прогресс реферальной программы</h3>
|
||
<div className="space-y-3">
|
||
<div>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className="text-sm text-slate-300">До следующего бонуса</span>
|
||
<span className="text-sm font-bold">{referralCount}/5</span>
|
||
</div>
|
||
<div className="h-3 bg-slate-900/50 rounded-full overflow-hidden">
|
||
<div
|
||
className="h-full bg-gradient-to-r from-purple-500 to-blue-500 transition-all"
|
||
style={{ width: `${(referralCount / 5) * 100}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<p className="text-sm text-slate-300">
|
||
{referralCount >= 5
|
||
? '🎁 Вы получили месяц в подарок!'
|
||
: `Пригласите еще ${5 - referralCount} друзей и получите месяц бесплатно!`}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Setup Guide Link */}
|
||
<Link href="/setup">
|
||
<div className="bg-slate-800/50 border border-slate-700 rounded-lg p-6 flex items-center justify-between hover:bg-slate-700 transition-colors">
|
||
<div className="flex items-center gap-4">
|
||
<Shield className="h-8 w-8 text-blue-500" />
|
||
<div>
|
||
<h3 className="font-bold">Инструкция по настройке</h3>
|
||
<p className="text-sm text-slate-400">Как подключить VPN на вашем устройстве</p>
|
||
</div>
|
||
</div>
|
||
<Settings className="h-6 w-6 text-slate-400" />
|
||
</div>
|
||
</Link>
|
||
|
||
{/* Spacer для BottomNav */}
|
||
<div className="h-20" />
|
||
</main>
|
||
|
||
{/* Modals */}
|
||
{isQROpen && subscriptionUrl && (
|
||
<QRCodeModal isOpen={isQROpen} url={subscriptionUrl} onClose={() => setIsQROpen(false)} />
|
||
)}
|
||
|
||
{isReferralOpen && (
|
||
<ReferralModal
|
||
isOpen={isReferralOpen}
|
||
onClose={() => setIsReferralOpen(false)}
|
||
referralUrl={`https://t.me/${process.env.NEXT_PUBLIC_TELEGRAM_BOT_USERNAME || 'Dorod_vps_bot'}?start=ref_${username}`}
|
||
onShare={shareReferralLink}
|
||
onCopy={() => copyToClipboard(`https://t.me/${process.env.NEXT_PUBLIC_TELEGRAM_BOT_USERNAME || 'Dorod_vps_bot'}?start=ref_${username}`)}
|
||
/>
|
||
)}
|
||
|
||
{/* Toast */}
|
||
{showToast && (
|
||
<div className="fixed bottom-24 left-1/2 -translate-x-1/2 bg-green-600 text-white px-6 py-3 rounded-lg shadow-lg z-50 animate-fade-in-up">
|
||
{toastMessage}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|