🏠 Page: Главная страница - статус подписки и действия
This commit is contained in:
592
app/page.tsx
Normal file
592
app/page.tsx
Normal file
@@ -0,0 +1,592 @@
|
||||
'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, MARZBAN_SUBSCRIPTION_URL } from '@/lib/constants';
|
||||
import {
|
||||
Shield,
|
||||
Settings,
|
||||
Gift,
|
||||
DollarSign,
|
||||
Wrench,
|
||||
User,
|
||||
HelpCircle,
|
||||
ExternalLink,
|
||||
X,
|
||||
Key,
|
||||
CreditCard,
|
||||
Lock,
|
||||
Eye,
|
||||
Ban,
|
||||
MessageCircle,
|
||||
UserPlus,
|
||||
QrCode,
|
||||
Copy,
|
||||
ChevronRight,
|
||||
Share2,
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const [hasSubscription, setHasSubscription] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isKeyMenuOpen, setIsKeyMenuOpen] = useState(false);
|
||||
const [isQROpen, setIsQROpen] = useState(false);
|
||||
const [isReferralOpen, setIsReferralOpen] = useState(false);
|
||||
const [subscriptionStatus, setSubscriptionStatus] = useState<'active' | 'expired' | 'none'>('none');
|
||||
const [expiryDate, setExpiryDate] = useState<string>('');
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [toastMessage, setToastMessage] = useState('');
|
||||
const [subscriptionToken, setSubscriptionToken] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const subscriptionUrl = subscriptionToken ? getSubscriptionUrl(subscriptionToken) : MARZBAN_SUBSCRIPTION_URL;
|
||||
|
||||
const showToastNotification = (message: string) => {
|
||||
setToastMessage(message);
|
||||
setShowToast(true);
|
||||
setTimeout(() => setShowToast(false), 3000);
|
||||
};
|
||||
|
||||
// Загружаем подписку при монтировании - ЧЕРЕЗ API, НЕ localStorage!
|
||||
useEffect(() => {
|
||||
const loadSubscription = async () => {
|
||||
// Получаем Telegram данные
|
||||
const telegramWebApp = (window as any).Telegram?.WebApp;
|
||||
const telegramId = telegramWebApp?.initDataUnsafe?.user?.id;
|
||||
const telegramUsername = telegramWebApp?.initDataUnsafe?.user?.username;
|
||||
|
||||
console.log('🔍 Loading subscription for:', { telegramId, telegramUsername });
|
||||
|
||||
if (!telegramId && !telegramUsername) {
|
||||
console.log('⚠️ No Telegram data - probably opened in browser');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Запрашиваем подписку по Telegram ID через API
|
||||
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();
|
||||
|
||||
console.log('📥 API response:', data);
|
||||
|
||||
if (data.success && data.hasSubscription) {
|
||||
setSubscriptionToken(data.token);
|
||||
setHasSubscription(true);
|
||||
const status = data.status === 'active' ? 'active' : 'expired';
|
||||
setSubscriptionStatus(status);
|
||||
if (data.expire) {
|
||||
setExpiryDate(marzbanApi.formatExpireDate(data.expire));
|
||||
}
|
||||
} else {
|
||||
console.log('❌ No subscription found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load subscription:', error);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
loadSubscription();
|
||||
}, []); // Загружаем при каждом монтировании компонента
|
||||
// ОТКЛЮЧАЕМ автоматическую проверку API при загрузке (чтобы не зависала страница)
|
||||
// API проверка будет только на странице /subscription/[token]
|
||||
// useEffect(() => {
|
||||
// async function checkSubscription() {
|
||||
// try {
|
||||
// const userData = await marzbanApi.getUserInfo(subscriptionToken);
|
||||
//
|
||||
// if (userData.status === 'active') {
|
||||
// setHasSubscription(true);
|
||||
// setSubscriptionStatus('active');
|
||||
// setExpiryDate(marzbanApi.formatExpireDate(userData.expire));
|
||||
// } else if (userData.status === 'expired') {
|
||||
// setHasSubscription(false);
|
||||
// setSubscriptionStatus('expired');
|
||||
// setExpiryDate(marzbanApi.formatExpireDate(userData.expire));
|
||||
// } else {
|
||||
// setHasSubscription(false);
|
||||
// setSubscriptionStatus('none');
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Failed to check subscription:', error);
|
||||
// setHasSubscription(false);
|
||||
// setSubscriptionStatus('none');
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// checkSubscription();
|
||||
// }, []);
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
showToastNotification('✅ Скопировано в буфер обмена!');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
showToastNotification('❌ Ошибка копирования');
|
||||
}
|
||||
};
|
||||
const shareReferralLink = async () => {
|
||||
// Генерируем реферальную ссылку (TODO: заменить на реальный user ID после авторизации)
|
||||
const userId = subscriptionToken?.split('_')[0] || 'DEMO';
|
||||
const referralUrl = `https://t.me/umbrix_bot?start=ref_${userId}`;
|
||||
const shareText = `🚀 Попробуй Umbrix VPN - быстрый и безопасный VPN!\n\n✨ Получи 7 дней бесплатно по моей ссылке:\n${referralUrl}`;
|
||||
|
||||
// Проверяем поддержку Web Share API
|
||||
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);
|
||||
// Fallback: копируем в буфер
|
||||
copyToClipboard(shareText);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback для браузеров без поддержки Share API
|
||||
copyToClipboard(shareText);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen flex flex-col"
|
||||
style={{ background: 'var(--bg-app)' }}
|
||||
>
|
||||
{/* 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)' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-6 h-6" style={{ color: 'var(--primary)' }} />
|
||||
<span
|
||||
className="text-xl font-bold"
|
||||
style={{ color: 'var(--text-white)' }}
|
||||
>
|
||||
Umbrix
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="p-2 rounded-lg"
|
||||
style={{ background: 'var(--bg-card)' }}
|
||||
>
|
||||
<Settings
|
||||
className="w-5 h-5"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
/>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* Бегущая строка с преимуществами */}
|
||||
<div
|
||||
className="w-full overflow-hidden border-b py-2"
|
||||
style={{ borderColor: 'var(--border)', background: 'var(--bg-card)' }}
|
||||
>
|
||||
<div className="animate-marquee whitespace-nowrap flex items-center gap-8 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4" style={{ color: 'var(--primary)' }} />
|
||||
<span className="text-slate-300">Без логов</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Lock className="w-4 h-4" style={{ color: 'var(--primary)' }} />
|
||||
<span className="text-slate-300">Без регистрации</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye className="w-4 h-4 line-through opacity-50" style={{ color: 'var(--primary)' }} />
|
||||
<span className="text-slate-300">Не отслеживаем</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Ban className="w-4 h-4" style={{ color: 'var(--primary)' }} />
|
||||
<span className="text-slate-300">Не продаём ваш трафик</span>
|
||||
</div>
|
||||
{/* Дублируем для бесшовной анимации */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4" style={{ color: 'var(--primary)' }} />
|
||||
<span className="text-slate-300">Без логов</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Lock className="w-4 h-4" style={{ color: 'var(--primary)' }} />
|
||||
<span className="text-slate-300">Без регистрации</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Eye className="w-4 h-4 line-through opacity-50" style={{ color: 'var(--primary)' }} />
|
||||
<span className="text-slate-300">Не отслеживаем</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Ban className="w-4 h-4" style={{ color: 'var(--primary)' }} />
|
||||
<span className="text-slate-300">Не продаём ваш трафик</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 flex flex-col items-center justify-center px-6 py-8">
|
||||
{/* Hero Section - Логотип и статус */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="text-6xl mb-4">🛡️</div>
|
||||
<h1 className="text-2xl font-bold mb-2">Umbrix VPN</h1>
|
||||
<p className="text-sm opacity-70">Быстрый и безопасный VPN</p>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="text-center mb-8">
|
||||
{hasSubscription ? (
|
||||
<Link
|
||||
href={`/subscription/${subscriptionToken}`}
|
||||
className="text-sm opacity-70 hover:opacity-100 transition-opacity cursor-pointer inline-block"
|
||||
>
|
||||
Моя подписка ✅ Активна до {expiryDate || '...'}
|
||||
</Link>
|
||||
) : subscriptionStatus === 'expired' ? (
|
||||
<div className="text-sm opacity-70 mb-1">
|
||||
⚠️ Подписка истекла {expiryDate}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm opacity-70 mb-1">
|
||||
⚠️ Нет подписки
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="w-full max-w-md space-y-3">
|
||||
{/* Показываем trial только если нет активной подписки */}
|
||||
{!hasSubscription && (
|
||||
<ActionButton
|
||||
icon={<Gift className="w-5 h-5" />}
|
||||
text="Попробовать 7 дней бесплатно"
|
||||
onClick={() => (window.location.href = '/plans')}
|
||||
/>
|
||||
)}
|
||||
<ActionButton
|
||||
icon={<DollarSign className="w-5 h-5" />}
|
||||
text="Купить подписку от 99₽"
|
||||
onClick={() => (window.location.href = '/plans')}
|
||||
/>
|
||||
<Link href="/setup">
|
||||
<ActionButton
|
||||
icon={<Wrench className="w-5 h-5" />}
|
||||
text="Настроить VPN"
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<nav
|
||||
className="border-t"
|
||||
style={{ borderColor: 'var(--border)', background: 'var(--bg-card)' }}
|
||||
>
|
||||
<div className="flex items-center justify-around py-3">
|
||||
<NavButton
|
||||
icon={<User className="w-6 h-6" />}
|
||||
label="Аккаунт"
|
||||
onClick={() => setIsMenuOpen(true)}
|
||||
/>
|
||||
<NavButton
|
||||
icon={<HelpCircle className="w-6 h-6" />}
|
||||
label="Помощь"
|
||||
href="/help"
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Account Menu Modal */}
|
||||
{isMenuOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-end"
|
||||
style={{ background: 'rgba(0, 0, 0, 0.5)' }}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<div
|
||||
className="w-full rounded-t-3xl p-6 pb-8"
|
||||
style={{ background: 'var(--bg-card)' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-bold" style={{ color: 'var(--text-white)' }}>
|
||||
Меню аккаунта
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className="p-2 rounded-lg hover:opacity-70 transition-opacity"
|
||||
style={{ background: 'var(--bg-elevated)' }}
|
||||
>
|
||||
<X className="w-5 h-5" style={{ color: 'var(--text-primary)' }} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Menu Items */}
|
||||
<div className="space-y-2">
|
||||
{hasSubscription ? (
|
||||
<>
|
||||
<Link href={`/subscription/${subscriptionToken}`}>
|
||||
<MenuButton
|
||||
icon={<Shield className="w-5 h-5" />}
|
||||
label="📊 Моя подписка"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<MenuButton
|
||||
icon={<Key className="w-5 h-5" />}
|
||||
label="🔑 Ключ подписки"
|
||||
onClick={() => {
|
||||
setIsMenuOpen(false);
|
||||
setIsKeyMenuOpen(true);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Link href="/plans">
|
||||
<MenuButton
|
||||
icon={<Shield className="w-5 h-5" />}
|
||||
label="🛒 Купить подписку"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link href="/plans">
|
||||
<MenuButton
|
||||
icon={<CreditCard className="w-5 h-5" />}
|
||||
label="💳 Тарифы"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<Link href="/settings">
|
||||
<MenuButton
|
||||
icon={<Settings className="w-5 h-5" />}
|
||||
label="⚙️ Настройки"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<MenuButton
|
||||
icon={<MessageCircle className="w-5 h-5" />}
|
||||
label="💬 Поддержка"
|
||||
onClick={() => {
|
||||
setIsMenuOpen(false);
|
||||
router.push('/help');
|
||||
}}
|
||||
/>
|
||||
|
||||
<MenuButton
|
||||
icon={<UserPlus className="w-5 h-5" />}
|
||||
label="🎁 Пригласить друга"
|
||||
onClick={() => {
|
||||
setIsMenuOpen(false);
|
||||
setIsReferralOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Key Subscription Menu Modal */}
|
||||
{isKeyMenuOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-end"
|
||||
style={{ background: 'rgba(0, 0, 0, 0.5)' }}
|
||||
onClick={() => setIsKeyMenuOpen(false)}
|
||||
>
|
||||
<div
|
||||
className="w-full rounded-t-3xl p-6 pb-8"
|
||||
style={{ background: 'var(--bg-card)' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-bold" style={{ color: 'var(--text-white)' }}>
|
||||
Ключ подписки
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setIsKeyMenuOpen(false)}
|
||||
className="p-2 rounded-lg hover:opacity-70 transition-opacity"
|
||||
style={{ background: 'var(--bg-elevated)' }}
|
||||
>
|
||||
<X className="w-5 h-5" style={{ color: 'var(--text-primary)' }} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Menu Items */}
|
||||
<div className="space-y-2">
|
||||
<MenuButton
|
||||
icon={<Copy className="w-5 h-5" />}
|
||||
label="Скопировать ссылку"
|
||||
onClick={() => {
|
||||
copyToClipboard(subscriptionUrl);
|
||||
setIsKeyMenuOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<MenuButton
|
||||
icon={<QrCode className="w-5 h-5" />}
|
||||
label="Показать QR код"
|
||||
onClick={() => {
|
||||
setIsKeyMenuOpen(false);
|
||||
setIsQROpen(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
<MenuButton
|
||||
icon={<ExternalLink className="w-5 h-5" />}
|
||||
label="Поделиться подпиской"
|
||||
onClick={() => {
|
||||
// TODO: Проверка тарифа, затем share sheet
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: 'VPN подписка',
|
||||
text: 'Моя VPN подписка',
|
||||
url: subscriptionUrl,
|
||||
});
|
||||
} else {
|
||||
copyToClipboard(subscriptionUrl);
|
||||
}
|
||||
setIsKeyMenuOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* QR Code Modal */}
|
||||
<QRCodeModal
|
||||
isOpen={isQROpen}
|
||||
onClose={() => setIsQROpen(false)}
|
||||
url={subscriptionUrl}
|
||||
title="QR код подписки"
|
||||
/>
|
||||
|
||||
{/* Referral Modal */}
|
||||
<ReferralModal
|
||||
isOpen={isReferralOpen}
|
||||
onClose={() => setIsReferralOpen(false)}
|
||||
referralUrl={`https://t.me/umbrix_bot?start=ref_${subscriptionToken?.split('_')[0] || 'DEMO'}`}
|
||||
onShare={() => {
|
||||
shareReferralLink();
|
||||
setIsReferralOpen(false);
|
||||
}}
|
||||
onCopy={() => {
|
||||
const userId = subscriptionToken?.split('_')[0] || 'DEMO';
|
||||
const referralUrl = `https://t.me/umbrix_bot?start=ref_${userId}`;
|
||||
copyToClipboard(referralUrl);
|
||||
setIsReferralOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Toast Notification */}
|
||||
{showToast && (
|
||||
<div className="fixed bottom-24 left-1/2 transform -translate-x-1/2 z-50 animate-fade-in">
|
||||
<div className="bg-slate-800 text-white px-6 py-3 rounded-full shadow-lg border border-slate-700 flex items-center gap-2">
|
||||
<span>{toastMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionButton({
|
||||
icon,
|
||||
text,
|
||||
onClick,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="w-full flex items-center gap-3 p-4 rounded-xl transition-all hover:opacity-80"
|
||||
style={{ background: 'var(--bg-card)', color: 'var(--text-white)' }}
|
||||
>
|
||||
<div style={{ color: 'var(--primary)' }}>{icon}</div>
|
||||
<span className="font-medium">{text}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function NavButton({
|
||||
icon,
|
||||
label,
|
||||
href,
|
||||
onClick,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="flex flex-col items-center gap-1 transition-all hover:opacity-80 cursor-pointer"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
>
|
||||
{icon}
|
||||
<span className="text-xs">{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={href!}>
|
||||
<div className="flex flex-col items-center gap-1 transition-all hover:opacity-80 cursor-pointer"
|
||||
style={{ color: 'var(--text-primary)' }}
|
||||
>
|
||||
{icon}
|
||||
<span className="text-xs">{label}</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuButton({
|
||||
icon,
|
||||
label,
|
||||
onClick,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="w-full flex items-center justify-between p-4 rounded-xl transition-all hover:opacity-80"
|
||||
style={{ background: 'var(--bg-elevated)', color: 'var(--text-white)' }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div style={{ color: 'var(--primary)' }}>{icon}</div>
|
||||
<span className="font-medium">{label}</span>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 opacity-50" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user