🔒 Аудит: безопасность, TypeScript, UI, BottomNav
Безопасность: - 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)
This commit is contained in:
304
app/dashboard/page.tsx
Normal file
304
app/dashboard/page.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user