💳 Page: Тарифы - выбор плана подписки VPN
This commit is contained in:
257
app/plans/page.tsx
Normal file
257
app/plans/page.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ArrowLeft, Check } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function Plans() {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handlePurchase = async (planType: string) => {
|
||||
// Проверяем есть ли уже активная подписка ЧЕРЕЗ API
|
||||
const telegramWebApp = (window as any).Telegram?.WebApp;
|
||||
const telegramId = telegramWebApp?.initDataUnsafe?.user?.id;
|
||||
const telegramUsername = telegramWebApp?.initDataUnsafe?.user?.username;
|
||||
|
||||
if (telegramId || telegramUsername) {
|
||||
const params = new URLSearchParams();
|
||||
if (telegramId) params.append('telegramId', telegramId.toString());
|
||||
if (telegramUsername) params.append('telegramUsername', telegramUsername);
|
||||
|
||||
const checkResponse = await fetch(`/api/user-subscription?${params.toString()}`);
|
||||
const checkData = await checkResponse.json();
|
||||
|
||||
if (checkData.success && checkData.hasSubscription) {
|
||||
const confirmOverwrite = confirm('У вас уже есть активная подписка. Создать новую? (старая будет заменена)');
|
||||
if (!confirmOverwrite) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const telegramWebApp = (window as any).Telegram?.WebApp;
|
||||
console.log('🔍 Telegram WebApp объект:', telegramWebApp);
|
||||
console.log('🔍 initData:', telegramWebApp?.initData);
|
||||
console.log('🔍 initDataUnsafe (FULL):', JSON.stringify(telegramWebApp?.initDataUnsafe, null, 2));
|
||||
console.log('🔍 user (FULL):', JSON.stringify(telegramWebApp?.initDataUnsafe?.user, null, 2));
|
||||
|
||||
const user = telegramWebApp?.initDataUnsafe?.user;
|
||||
const telegramId = user?.id || null;
|
||||
const telegramUsername = user?.username || null;
|
||||
const firstName = user?.first_name || null;
|
||||
const lastName = user?.last_name || null;
|
||||
|
||||
console.log('📤 Отправляем:', { planType, telegramId, telegramUsername, firstName, lastName });
|
||||
|
||||
// Вызываем API для создания реального пользователя в Marzban
|
||||
const response = await fetch('/api/create-user', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
planType: planType,
|
||||
telegramId: telegramId,
|
||||
telegramUsername: telegramUsername,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to create subscription');
|
||||
}
|
||||
|
||||
// Просто возвращаемся на главную - там данные обновятся автоматически
|
||||
router.push('/');
|
||||
} catch (error) {
|
||||
console.error('Purchase error:', error);
|
||||
alert('Ошибка при создании подписки. Попробуйте позже.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen"
|
||||
style={{ background: 'var(--bg-app)', color: 'var(--text-primary)' }}
|
||||
>
|
||||
{/* Header */}
|
||||
<header
|
||||
className="flex items-center gap-3 p-4 border-b sticky top-0 z-10 backdrop-blur-sm"
|
||||
style={{ borderColor: 'var(--border)', background: 'var(--bg-card)' }}
|
||||
>
|
||||
<button onClick={() => router.push('/')} className="p-2 hover:bg-slate-700 rounded-lg transition-colors">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<h1
|
||||
className="text-xl font-bold"
|
||||
style={{ color: 'var(--text-white)' }}
|
||||
>
|
||||
Выбрать тариф
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<main className="p-4">
|
||||
{/* Trial - полная ширина */}
|
||||
<div className="mb-3">
|
||||
<PlanCard
|
||||
badge="🎁 Пробный период"
|
||||
title="7 дней бесплатно"
|
||||
price="0₽"
|
||||
features={[
|
||||
'Все тарифы доступны',
|
||||
'Безлимитный трафик',
|
||||
'Любые локации',
|
||||
]}
|
||||
buttonText="Попробовать"
|
||||
isPrimary
|
||||
onPurchase={() => handlePurchase('trial')}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Grid 2x2 для остальных тарифов */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Start */}
|
||||
<PlanCard
|
||||
badge="🌍 Старт"
|
||||
title="Базовый"
|
||||
price="100₽/мес"
|
||||
features={['1 устройство', '1 локация', '50 ГБ']}
|
||||
buttonText="Выбрать"
|
||||
onPurchase={() => handlePurchase('start')}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
{/* Plus */}
|
||||
<PlanCard
|
||||
badge="🌎 Плюс"
|
||||
title="Расширенный"
|
||||
price="299₽/мес"
|
||||
features={['3 устройства', '3 локации', '299 ГБ']}
|
||||
buttonText="Выбрать"
|
||||
onPurchase={() => handlePurchase('plus')}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
{/* Max */}
|
||||
<PlanCard
|
||||
badge="🌏 Макс"
|
||||
title="Премиум"
|
||||
price="350₽/мес"
|
||||
features={['5 устройств', '15+ стран', 'Безлимит']}
|
||||
buttonText="Выбрать"
|
||||
isPopular
|
||||
onPurchase={() => handlePurchase('max')}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
{/* Empty slot or promo */}
|
||||
<div
|
||||
className="p-4 rounded-xl border flex items-center justify-center"
|
||||
style={{
|
||||
background: 'var(--bg-card)',
|
||||
borderColor: 'var(--border)',
|
||||
borderStyle: 'dashed',
|
||||
}}
|
||||
>
|
||||
<div className="text-center opacity-50">
|
||||
<div className="text-2xl mb-1">💰</div>
|
||||
<div className="text-xs">Скоро новые</div>
|
||||
<div className="text-xs">тарифы</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PlanCard({
|
||||
badge,
|
||||
title,
|
||||
price,
|
||||
features,
|
||||
buttonText,
|
||||
isPrimary = false,
|
||||
isPopular = false,
|
||||
onPurchase,
|
||||
isLoading = false,
|
||||
}: {
|
||||
badge: string;
|
||||
title: string;
|
||||
price: string;
|
||||
features: string[];
|
||||
buttonText: string;
|
||||
isPrimary?: boolean;
|
||||
isPopular?: boolean;
|
||||
onPurchase: () => void;
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="p-3 rounded-xl border relative"
|
||||
style={{
|
||||
background: 'var(--bg-card)',
|
||||
borderColor: isPrimary
|
||||
? 'var(--primary)'
|
||||
: isPopular
|
||||
? 'var(--primary)'
|
||||
: 'var(--border)',
|
||||
}}
|
||||
>
|
||||
{isPopular && (
|
||||
<div
|
||||
className="absolute -top-2 -right-2 px-2 py-0.5 rounded-full text-xs font-bold"
|
||||
style={{ background: 'var(--primary)', color: 'var(--text-white)' }}
|
||||
>
|
||||
⭐ ТОП
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs mb-1 opacity-70">{badge}</div>
|
||||
<h3
|
||||
className="font-bold mb-1 text-sm"
|
||||
style={{ color: 'var(--text-white)' }}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<div
|
||||
className="text-xl font-bold mb-2"
|
||||
style={{ color: 'var(--primary)' }}
|
||||
>
|
||||
{price}
|
||||
</div>
|
||||
|
||||
<ul className="space-y-1.5 mb-3">
|
||||
{features.map((feature, i) => (
|
||||
<li key={i} className="flex items-center gap-1.5 text-xs">
|
||||
<Check
|
||||
className="w-3.5 h-3.5 flex-shrink-0"
|
||||
style={{ color: 'var(--success)' }}
|
||||
/>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
onClick={onPurchase}
|
||||
disabled={isLoading}
|
||||
className="w-full py-2 rounded-lg font-semibold text-xs transition-all hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: isPrimary ? 'var(--primary)' : 'var(--bg-elevated)',
|
||||
color: 'var(--text-white)',
|
||||
}}
|
||||
>
|
||||
{isLoading ? 'Оформление...' : buttonText}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user