Files
app_umbrix/app/page.tsx

597 lines
21 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
// Главная страница приложения (Home Page)
// Показывает статус подписки пользователя и кнопки действий
// URL: https://app.umbrix.net/
'use client'; // Next.js 13: это клиентский компонент (с useState, useEffect)
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>
);
}