2026-02-04 05:20:11 +03:00
|
|
|
|
'use client';
|
|
|
|
|
|
|
2026-02-08 18:59:02 +03:00
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
|
import { ArrowLeft, Check, ChevronRight, Loader2 } from 'lucide-react';
|
2026-02-04 05:20:11 +03:00
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
|
|
|
2026-02-08 18:59:02 +03:00
|
|
|
|
type PlanType = 'trial' | 'start' | 'plus' | 'max';
|
|
|
|
|
|
type Period = '1month' | '3months' | '6months' | '1year';
|
|
|
|
|
|
type Step = 'plan' | 'period' | 'locations' | 'processing';
|
|
|
|
|
|
|
|
|
|
|
|
interface Location {
|
|
|
|
|
|
id: number;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
address: string;
|
|
|
|
|
|
ping: string;
|
|
|
|
|
|
country: string;
|
|
|
|
|
|
flag: string;
|
|
|
|
|
|
status: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface PlanConfig {
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
badge: string;
|
|
|
|
|
|
maxLocations: number;
|
|
|
|
|
|
dataLimit: string;
|
|
|
|
|
|
prices: Record<Period, number>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const PLANS: Record<PlanType, PlanConfig> = {
|
|
|
|
|
|
trial: {
|
|
|
|
|
|
name: 'Пробный',
|
|
|
|
|
|
badge: '🎁 Пробный период',
|
|
|
|
|
|
maxLocations: 999,
|
|
|
|
|
|
dataLimit: 'Безлимит',
|
|
|
|
|
|
prices: {
|
|
|
|
|
|
'1month': 0,
|
|
|
|
|
|
'3months': 0,
|
|
|
|
|
|
'6months': 0,
|
|
|
|
|
|
'1year': 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
start: {
|
|
|
|
|
|
name: 'Старт',
|
|
|
|
|
|
badge: '🌍 Старт',
|
|
|
|
|
|
maxLocations: 1,
|
|
|
|
|
|
dataLimit: '50 ГБ',
|
|
|
|
|
|
prices: {
|
2026-02-08 23:54:30 +03:00
|
|
|
|
'1month': 99,
|
|
|
|
|
|
'3months': 269, // -9% (89₽/мес)
|
|
|
|
|
|
'6months': 499, // -16% (83₽/мес)
|
|
|
|
|
|
'1year': 949, // -20% (79₽/мес)
|
2026-02-08 18:59:02 +03:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
plus: {
|
|
|
|
|
|
name: 'Плюс',
|
|
|
|
|
|
badge: '🌎 Плюс',
|
|
|
|
|
|
maxLocations: 3,
|
|
|
|
|
|
dataLimit: '299 ГБ',
|
|
|
|
|
|
prices: {
|
|
|
|
|
|
'1month': 249,
|
|
|
|
|
|
'3months': 649, // -13% (216₽/мес)
|
|
|
|
|
|
'6months': 1199, // -20% (199₽/мес)
|
|
|
|
|
|
'1year': 2249, // -25% (187₽/мес)
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
max: {
|
|
|
|
|
|
name: 'Макс',
|
|
|
|
|
|
badge: '🌏 Макс',
|
|
|
|
|
|
maxLocations: 0, // Все локации автоматически, без выбора
|
|
|
|
|
|
dataLimit: 'Безлимит',
|
|
|
|
|
|
prices: {
|
|
|
|
|
|
'1month': 350,
|
|
|
|
|
|
'3months': 949, // -10% (316₽/мес)
|
|
|
|
|
|
'6months': 1799, // -14% (299₽/мес)
|
|
|
|
|
|
'1year': 3349, // -20% (279₽/мес)
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const PERIOD_LABELS: Record<Period, { label: string; months: number }> = {
|
|
|
|
|
|
'1month': { label: '1 месяц', months: 1 },
|
|
|
|
|
|
'3months': { label: '3 месяца', months: 3 },
|
|
|
|
|
|
'6months': { label: '6 месяцев', months: 6 },
|
|
|
|
|
|
'1year': { label: '1 год', months: 12 },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default function PlansNew() {
|
2026-02-04 05:20:11 +03:00
|
|
|
|
const router = useRouter();
|
2026-02-08 18:59:02 +03:00
|
|
|
|
const [step, setStep] = useState<Step>('plan');
|
|
|
|
|
|
const [selectedPlan, setSelectedPlan] = useState<PlanType | null>(null);
|
|
|
|
|
|
const [selectedPeriod, setSelectedPeriod] = useState<Period>('1month');
|
|
|
|
|
|
const [selectedLocations, setSelectedLocations] = useState<number[]>([]);
|
|
|
|
|
|
const [availableLocations, setAvailableLocations] = useState<Location[]>([]);
|
|
|
|
|
|
const [isLoadingLocations, setIsLoadingLocations] = useState(false);
|
|
|
|
|
|
const [isCreatingUser, setIsCreatingUser] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
// Загрузить локации при переходе к шагу выбора
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (step === 'locations' && availableLocations.length === 0) {
|
|
|
|
|
|
loadLocations();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [step]);
|
|
|
|
|
|
|
|
|
|
|
|
async function loadLocations() {
|
|
|
|
|
|
setIsLoadingLocations(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/nodes');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
setAvailableLocations(data.locations || []);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error('Failed to load locations:', data.error);
|
2026-02-04 05:20:11 +03:00
|
|
|
|
}
|
2026-02-08 18:59:02 +03:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error loading locations:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoadingLocations(false);
|
2026-02-04 05:20:11 +03:00
|
|
|
|
}
|
2026-02-08 18:59:02 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handlePlanSelect(plan: PlanType) {
|
|
|
|
|
|
setSelectedPlan(plan);
|
2026-02-04 05:20:11 +03:00
|
|
|
|
|
2026-02-08 18:59:02 +03:00
|
|
|
|
// Trial период - сразу создаем без выбора периода и локаций
|
|
|
|
|
|
if (plan === 'trial') {
|
|
|
|
|
|
createUser(plan, '1month', []);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setStep('period');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handlePeriodSelect(period: Period) {
|
|
|
|
|
|
setSelectedPeriod(period);
|
|
|
|
|
|
|
|
|
|
|
|
// Только для PLUS показываем выбор локаций
|
|
|
|
|
|
// Для остальных тарифов - все локации автоматически
|
|
|
|
|
|
if (selectedPlan === 'plus') {
|
|
|
|
|
|
setStep('locations');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Start и Max - создаем сразу со всеми локациями
|
|
|
|
|
|
createUser(selectedPlan!, period, []);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleLocationToggle(locationId: number) {
|
|
|
|
|
|
if (!selectedPlan) return;
|
|
|
|
|
|
|
|
|
|
|
|
const maxLocations = PLANS[selectedPlan].maxLocations;
|
2026-02-04 05:20:11 +03:00
|
|
|
|
|
2026-02-08 18:59:02 +03:00
|
|
|
|
if (selectedLocations.includes(locationId)) {
|
|
|
|
|
|
setSelectedLocations(selectedLocations.filter(id => id !== locationId));
|
|
|
|
|
|
} else if (maxLocations === 999 || selectedLocations.length < maxLocations) {
|
|
|
|
|
|
setSelectedLocations([...selectedLocations, locationId]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function createUser(planType: PlanType, period: Period, locationIds: number[]) {
|
|
|
|
|
|
setStep('processing');
|
|
|
|
|
|
setIsCreatingUser(true);
|
|
|
|
|
|
|
2026-02-04 05:20:11 +03:00
|
|
|
|
try {
|
|
|
|
|
|
const telegramWebApp = (window as any).Telegram?.WebApp;
|
|
|
|
|
|
const user = telegramWebApp?.initDataUnsafe?.user;
|
|
|
|
|
|
|
2026-02-09 05:06:46 +03:00
|
|
|
|
console.log('🔍 TELEGRAM USER DATA:', {
|
|
|
|
|
|
hasWebApp: !!telegramWebApp,
|
|
|
|
|
|
hasInitData: !!telegramWebApp?.initDataUnsafe,
|
|
|
|
|
|
hasUser: !!user,
|
|
|
|
|
|
user: user ? {
|
|
|
|
|
|
id: user.id,
|
|
|
|
|
|
username: user.username,
|
|
|
|
|
|
first_name: user.first_name,
|
|
|
|
|
|
last_name: user.last_name,
|
|
|
|
|
|
} : null
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const requestBody = {
|
|
|
|
|
|
planType,
|
|
|
|
|
|
period,
|
|
|
|
|
|
locationIds,
|
|
|
|
|
|
telegramId: user?.id || null,
|
|
|
|
|
|
telegramUsername: user?.username || null,
|
|
|
|
|
|
firstName: user?.first_name || null,
|
|
|
|
|
|
lastName: user?.last_name || null,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
console.log('📤 REQUEST BODY:', requestBody);
|
|
|
|
|
|
|
2026-02-04 05:20:11 +03:00
|
|
|
|
const response = await fetch('/api/create-user', {
|
|
|
|
|
|
method: 'POST',
|
2026-02-08 18:59:02 +03:00
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
2026-02-09 05:06:46 +03:00
|
|
|
|
body: JSON.stringify(requestBody),
|
2026-02-04 05:20:11 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
2026-02-08 18:59:02 +03:00
|
|
|
|
if (data.success) {
|
2026-02-09 04:56:14 +03:00
|
|
|
|
// Успех - возвращаемся на главную с параметром обновления
|
|
|
|
|
|
router.push('/?refresh=true');
|
2026-02-08 18:59:02 +03:00
|
|
|
|
} else {
|
2026-02-04 05:20:11 +03:00
|
|
|
|
throw new Error(data.error || 'Failed to create subscription');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-02-08 18:59:02 +03:00
|
|
|
|
console.error('Create user error:', error);
|
2026-02-04 05:20:11 +03:00
|
|
|
|
alert('Ошибка при создании подписки. Попробуйте позже.');
|
2026-02-08 18:59:02 +03:00
|
|
|
|
setStep('plan');
|
2026-02-04 05:20:11 +03:00
|
|
|
|
} finally {
|
2026-02-08 18:59:02 +03:00
|
|
|
|
setIsCreatingUser(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handlePurchase() {
|
|
|
|
|
|
if (!selectedPlan || selectedLocations.length === 0) {
|
|
|
|
|
|
alert('Выберите хотя бы одну локацию');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
createUser(selectedPlan, selectedPeriod, selectedLocations);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function calculateDiscount(period: Period): number {
|
|
|
|
|
|
const discounts = {
|
|
|
|
|
|
'1month': 0,
|
|
|
|
|
|
'3months': 10,
|
|
|
|
|
|
'6months': 15,
|
|
|
|
|
|
'1year': 20,
|
|
|
|
|
|
};
|
|
|
|
|
|
return discounts[period];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function calculateSavings(plan: PlanType, period: Period): number {
|
|
|
|
|
|
if (plan === 'trial') return 0;
|
|
|
|
|
|
|
|
|
|
|
|
const config = PLANS[plan];
|
|
|
|
|
|
const monthlyPrice = config.prices['1month'];
|
|
|
|
|
|
const months = PERIOD_LABELS[period].months;
|
|
|
|
|
|
const fullPrice = monthlyPrice * months;
|
|
|
|
|
|
const discountedPrice = config.prices[period];
|
|
|
|
|
|
return fullPrice - discountedPrice;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function goBack() {
|
|
|
|
|
|
if (step === 'period') {
|
|
|
|
|
|
setStep('plan');
|
|
|
|
|
|
} else if (step === 'locations') {
|
|
|
|
|
|
setStep('period');
|
|
|
|
|
|
} else if (step === 'processing') {
|
|
|
|
|
|
setStep('locations');
|
2026-02-04 05:20:11 +03:00
|
|
|
|
}
|
2026-02-08 18:59:02 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const canProceedFromLocations =
|
|
|
|
|
|
selectedPlan &&
|
|
|
|
|
|
(PLANS[selectedPlan].maxLocations === 999 || selectedLocations.length >= PLANS[selectedPlan].maxLocations);
|
|
|
|
|
|
|
2026-02-04 05:20:11 +03:00
|
|
|
|
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)' }}
|
|
|
|
|
|
>
|
2026-02-08 18:59:02 +03:00
|
|
|
|
{step === 'plan' ? (
|
|
|
|
|
|
<button onClick={() => router.push('/')} className="p-2 hover:bg-slate-700 rounded-lg transition-colors">
|
|
|
|
|
|
<ArrowLeft className="w-6 h-6" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : step !== 'processing' && (
|
|
|
|
|
|
<button onClick={goBack} 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)' }}>
|
|
|
|
|
|
{step === 'plan' && 'Выбор тарифа'}
|
|
|
|
|
|
{step === 'period' && `Период подписки${selectedPlan ? ` · ${PLANS[selectedPlan].name}` : ''}`}
|
|
|
|
|
|
{step === 'locations' && 'Выбор локаций'}
|
|
|
|
|
|
{step === 'processing' && 'Создание подписки...'}
|
2026-02-04 05:20:11 +03:00
|
|
|
|
</h1>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
2026-02-08 18:59:02 +03:00
|
|
|
|
<main className="p-4 pb-24">
|
|
|
|
|
|
{/* Шаг 1: Выбор тарифа */}
|
|
|
|
|
|
{step === 'plan' && (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
{/* Trial - полная ширина */}
|
|
|
|
|
|
<PlanCard
|
|
|
|
|
|
{...PLANS.trial}
|
|
|
|
|
|
title="7 дней бесплатно"
|
|
|
|
|
|
price="0₽"
|
|
|
|
|
|
features={['Все тарифы доступны', 'Безлимитный трафик', 'Любые локации']}
|
|
|
|
|
|
buttonText="Попробовать"
|
|
|
|
|
|
isPrimary
|
|
|
|
|
|
onSelect={() => handlePlanSelect('trial')}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Grid 2x2 для остальных */}
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
|
{(['start', 'plus', 'max'] as PlanType[]).map((planKey) => {
|
|
|
|
|
|
const plan = PLANS[planKey];
|
|
|
|
|
|
return (
|
|
|
|
|
|
<PlanCard
|
|
|
|
|
|
key={planKey}
|
|
|
|
|
|
{...plan}
|
|
|
|
|
|
title={plan.name}
|
|
|
|
|
|
price={`${plan.prices['1month']}₽/мес`}
|
|
|
|
|
|
features={[
|
|
|
|
|
|
plan.maxLocations === 999 ? 'Все локации' : `${plan.maxLocations} локаций`,
|
|
|
|
|
|
plan.dataLimit,
|
|
|
|
|
|
'Безлимитная скорость',
|
|
|
|
|
|
]}
|
|
|
|
|
|
buttonText="Выбрать"
|
|
|
|
|
|
isPopular={planKey === 'max'}
|
|
|
|
|
|
onSelect={() => handlePlanSelect(planKey)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2026-02-04 05:20:11 +03:00
|
|
|
|
|
2026-02-08 18:59:02 +03:00
|
|
|
|
{/* Empty slot */}
|
|
|
|
|
|
<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>
|
2026-02-04 05:20:11 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-08 18:59:02 +03:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Шаг 2: Выбор периода */}
|
|
|
|
|
|
{step === 'period' && selectedPlan && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{/* Информация о тарифе */}
|
|
|
|
|
|
<div className="mb-4 p-3 rounded-lg" style={{ background: 'var(--bg-card)' }}>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="text-sm text-slate-400">Выбран тариф</div>
|
|
|
|
|
|
<div className="text-lg font-bold">{PLANS[selectedPlan].name}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-right">
|
|
|
|
|
|
<div className="text-xs text-slate-400">Локации</div>
|
|
|
|
|
|
<div className="text-lg font-bold">
|
|
|
|
|
|
{selectedPlan === 'plus' ? '3 на выбор' : 'Все'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-right">
|
|
|
|
|
|
<div className="text-xs text-slate-400">Трафик</div>
|
|
|
|
|
|
<div className="text-lg font-bold">{PLANS[selectedPlan].dataLimit}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Grid 2x2 для периодов */}
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
|
{Object.entries(PERIOD_LABELS).map(([periodKey, { label, months }]) => {
|
|
|
|
|
|
const period = periodKey as Period;
|
|
|
|
|
|
const price = PLANS[selectedPlan].prices[period];
|
|
|
|
|
|
const discount = calculateDiscount(period);
|
|
|
|
|
|
const savings = calculateSavings(selectedPlan, period);
|
|
|
|
|
|
const isRecommended = period === '6months'; // Самый выгодный
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={period}
|
|
|
|
|
|
onClick={() => handlePeriodSelect(period)}
|
|
|
|
|
|
className="p-4 rounded-xl border-2 text-center transition-all active:scale-95 relative hover:opacity-90"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: isRecommended ? 'linear-gradient(135deg, rgba(47, 190, 165, 0.15) 0%, rgba(68, 163, 52, 0.15) 100%)' : 'var(--bg-card)',
|
|
|
|
|
|
borderColor: selectedPeriod === period ? 'var(--primary)' : 'var(--border)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Badge "выгодно" */}
|
|
|
|
|
|
{isRecommended && (
|
|
|
|
|
|
<div className="absolute -top-2 left-1/2 -translate-x-1/2 px-2 py-0.5 bg-green-500 text-white text-xs rounded-full">
|
|
|
|
|
|
выгодно
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Период */}
|
|
|
|
|
|
<div className="text-base font-semibold mb-1">{label}</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Старая цена */}
|
|
|
|
|
|
{discount > 0 && (
|
|
|
|
|
|
<div className="text-xs text-slate-500 line-through mb-1">
|
|
|
|
|
|
{PLANS[selectedPlan].prices['1month'] * months} ₽
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Новая цена */}
|
|
|
|
|
|
<div className="text-2xl font-bold mb-1">{price} ₽</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Скидка */}
|
|
|
|
|
|
{discount > 0 && (
|
|
|
|
|
|
<div className="text-xs px-2 py-1 bg-green-500/20 text-green-400 rounded inline-block">
|
|
|
|
|
|
-{discount}%
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Цена за месяц */}
|
|
|
|
|
|
{months > 1 && (
|
|
|
|
|
|
<div className="text-xs text-slate-400 mt-2">
|
|
|
|
|
|
{Math.round(price / months)} ₽/мес
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Шаг 3: Выбор локаций */}
|
|
|
|
|
|
{step === 'locations' && selectedPlan && (
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div className="text-sm text-slate-400">
|
|
|
|
|
|
{PLANS[selectedPlan].maxLocations === 999
|
|
|
|
|
|
? 'Доступны все локации'
|
|
|
|
|
|
: `Выберите до ${PLANS[selectedPlan].maxLocations} локаций`}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{isLoadingLocations ? (
|
|
|
|
|
|
<div className="text-center py-8">
|
|
|
|
|
|
<Loader2 className="w-8 h-8 animate-spin mx-auto text-blue-500" />
|
|
|
|
|
|
<p className="mt-2 text-slate-400">Загрузка локаций...</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
{availableLocations.map((location) => {
|
|
|
|
|
|
const isSelected = selectedLocations.includes(location.id);
|
|
|
|
|
|
const isDisabled =
|
|
|
|
|
|
!isSelected &&
|
|
|
|
|
|
PLANS[selectedPlan].maxLocations !== 999 &&
|
|
|
|
|
|
selectedLocations.length >= PLANS[selectedPlan].maxLocations;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={location.id}
|
|
|
|
|
|
onClick={() => handleLocationToggle(location.id)}
|
|
|
|
|
|
disabled={isDisabled}
|
|
|
|
|
|
className={`w-full p-4 rounded-xl border transition-colors ${
|
|
|
|
|
|
isSelected
|
|
|
|
|
|
? 'bg-blue-600/20 border-blue-500'
|
|
|
|
|
|
: isDisabled
|
|
|
|
|
|
? 'opacity-50 cursor-not-allowed'
|
|
|
|
|
|
: 'hover:border-slate-600'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: isSelected ? undefined : 'var(--bg-card)',
|
|
|
|
|
|
borderColor: isSelected ? undefined : 'var(--border)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex justify-between items-center">
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
{isSelected && <Check className="w-5 h-5 text-blue-400" />}
|
|
|
|
|
|
<span className="text-lg">{location.flag}</span>
|
|
|
|
|
|
<span className="font-medium">{location.name}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="text-sm text-slate-400">{location.ping}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{selectedLocations.length > 0 && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handlePurchase}
|
|
|
|
|
|
className="w-full py-4 rounded-xl font-bold mt-6 flex items-center justify-center gap-2 transition-opacity hover:opacity-80"
|
|
|
|
|
|
style={{ background: 'var(--primary)', color: 'var(--text-white)' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
Создать подписку {PLANS[selectedPlan].prices[selectedPeriod]}₽
|
|
|
|
|
|
<ChevronRight className="w-5 h-5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Шаг 4: Processing */}
|
|
|
|
|
|
{step === 'processing' && (
|
|
|
|
|
|
<div className="text-center py-12">
|
|
|
|
|
|
<Loader2 className="w-16 h-16 animate-spin mx-auto mb-4" style={{ color: 'var(--primary)' }} />
|
|
|
|
|
|
<h2 className="text-xl font-bold mb-2">Создаем вашу подписку...</h2>
|
|
|
|
|
|
<p className="text-slate-400">Это займет несколько секунд</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Spacer для нижней навигации */}
|
|
|
|
|
|
<div className="h-20" />
|
2026-02-04 05:20:11 +03:00
|
|
|
|
</main>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 18:59:02 +03:00
|
|
|
|
// PlanCard Component
|
2026-02-04 05:20:11 +03:00
|
|
|
|
function PlanCard({
|
|
|
|
|
|
badge,
|
|
|
|
|
|
title,
|
|
|
|
|
|
price,
|
|
|
|
|
|
features,
|
|
|
|
|
|
buttonText,
|
|
|
|
|
|
isPrimary = false,
|
|
|
|
|
|
isPopular = false,
|
2026-02-08 18:59:02 +03:00
|
|
|
|
onSelect,
|
2026-02-04 05:20:11 +03:00
|
|
|
|
}: {
|
|
|
|
|
|
badge: string;
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
price: string;
|
|
|
|
|
|
features: string[];
|
|
|
|
|
|
buttonText: string;
|
|
|
|
|
|
isPrimary?: boolean;
|
|
|
|
|
|
isPopular?: boolean;
|
2026-02-08 18:59:02 +03:00
|
|
|
|
onSelect: () => void;
|
2026-02-04 05:20:11 +03:00
|
|
|
|
}) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
2026-02-08 18:59:02 +03:00
|
|
|
|
className={`p-4 rounded-xl border relative`}
|
2026-02-04 05:20:11 +03:00
|
|
|
|
style={{
|
|
|
|
|
|
background: 'var(--bg-card)',
|
2026-02-08 18:59:02 +03:00
|
|
|
|
borderColor: isPopular ? 'var(--primary)' : 'var(--border)',
|
2026-02-04 05:20:11 +03:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isPopular && (
|
2026-02-08 18:59:02 +03:00
|
|
|
|
<div className="absolute -top-2 left-1/2 -translate-x-1/2 px-3 py-1 rounded-full text-xs font-bold" style={{ background: 'var(--primary)', color: 'var(--text-white)' }}>
|
|
|
|
|
|
Популярный
|
2026-02-04 05:20:11 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-02-08 18:59:02 +03:00
|
|
|
|
<div className="text-sm opacity-80 mb-1">{badge}</div>
|
|
|
|
|
|
<h3 className="text-xl font-bold mb-1">{title}</h3>
|
|
|
|
|
|
<p className="text-2xl font-bold mb-3" style={{ color: 'var(--primary)' }}>
|
2026-02-04 05:20:11 +03:00
|
|
|
|
{price}
|
2026-02-08 18:59:02 +03:00
|
|
|
|
</p>
|
|
|
|
|
|
<ul className="space-y-2 mb-4">
|
|
|
|
|
|
{features.map((feature, index) => (
|
|
|
|
|
|
<li key={index} className="flex items-center gap-2 text-sm">
|
|
|
|
|
|
<Check className="w-4 h-4 text-green-500 flex-shrink-0" />
|
2026-02-04 05:20:11 +03:00
|
|
|
|
<span>{feature}</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<button
|
2026-02-08 18:59:02 +03:00
|
|
|
|
onClick={onSelect}
|
|
|
|
|
|
className="w-full py-2 rounded-lg font-medium transition-opacity hover:opacity-80"
|
2026-02-04 05:20:11 +03:00
|
|
|
|
style={{
|
|
|
|
|
|
background: isPrimary ? 'var(--primary)' : 'var(--bg-elevated)',
|
2026-02-08 18:59:02 +03:00
|
|
|
|
color: 'var(--text-white)'
|
2026-02-04 05:20:11 +03:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-02-08 18:59:02 +03:00
|
|
|
|
{buttonText}
|
2026-02-04 05:20:11 +03:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|