Files
app_umbrix/components/SetupWizard.tsx
Umbrix Dev 2e1ec469fc 🎨 Применены цвета Umbrix brand ко всем элементам Setup Wizard
- Прогресс бар: #2fbea5 (primary)
- Кнопки устройств: teal hover
- Блоки скачивания: rgba primary bg
- Location cards: teal selection
- Final step кнопки: primary colors
- Все текст цвета: CSS variables
2026-02-06 01:11:44 +03:00

479 lines
23 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.
// Setup Wizard - пошаговая настройка после создания подписки
'use client';
import { useState } from 'react';
import { X, ChevronLeft, Laptop, Smartphone, Check, Copy, QrCode } from 'lucide-react';
interface SetupWizardProps {
isOpen: boolean;
onClose: () => void;
subscriptionUrl: string;
username: string;
planType: 'trial' | 'basic' | 'extended' | 'premium';
expiryDate?: string;
}
type Step = 'device' | 'app-check' | 'location' | 'final';
type DeviceType = 'desktop' | 'mobile' | null;
type MobileOS = 'android' | 'ios' | null;
export default function SetupWizard({
isOpen,
onClose,
subscriptionUrl,
username,
planType,
expiryDate
}: SetupWizardProps) {
const [step, setStep] = useState<Step>('device');
const [deviceType, setDeviceType] = useState<DeviceType>(null);
const [mobileOS, setMobileOS] = useState<MobileOS>(null);
const [hasApp, setHasApp] = useState<boolean | null>(null);
const [selectedLocations, setSelectedLocations] = useState<string[]>([]);
const [copied, setCopied] = useState(false);
const [showQR, setShowQR] = useState(false);
// Локации для выбора (для тарифа "Расширенный")
const locations = [
{ id: 'nl', name: '🇳🇱 Нидерланды', ping: '15ms' },
{ id: 'de', name: '🇩🇪 Германия', ping: '20ms' },
{ id: 'us', name: '🇺🇸 США', ping: '120ms' },
{ id: 'sg', name: '🇸🇬 Сингапур', ping: '180ms' },
{ id: 'jp', name: '🇯🇵 Япония', ping: '160ms' },
{ id: 'uk', name: '🇬🇧 Великобритания', ping: '35ms' },
];
const needsLocationSelection = planType === 'extended';
const maxLocations = planType === 'extended' ? 3 : 1;
const handleCopy = async () => {
await navigator.clipboard.writeText(subscriptionUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleDeviceSelect = (type: DeviceType) => {
setDeviceType(type);
if (type === 'desktop') {
setStep('app-check');
} else {
setStep('app-check');
}
};
const handleAppCheck = (installed: boolean) => {
setHasApp(installed);
if (needsLocationSelection) {
setStep('location');
} else {
setStep('final');
}
};
const handleLocationToggle = (locationId: string) => {
if (selectedLocations.includes(locationId)) {
setSelectedLocations(selectedLocations.filter(id => id !== locationId));
} else if (selectedLocations.length < maxLocations) {
setSelectedLocations([...selectedLocations, locationId]);
}
};
const canProceedFromLocation = selectedLocations.length === maxLocations;
const getProgressSteps = () => {
const steps = ['device', 'app-check'];
if (needsLocationSelection) steps.push('location');
steps.push('final');
return steps;
};
const currentStepIndex = getProgressSteps().indexOf(step);
const totalSteps = getProgressSteps().length;
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="rounded-2xl max-w-md w-full relative border shadow-2xl" style={{ background: 'var(--bg-card)', borderColor: 'var(--border)' }}>
{/* Header */}
<div className="p-6 border-b" style={{ borderColor: 'var(--border)' }}>
<div className="flex items-center justify-between mb-4">
<button
onClick={() => {
if (step === 'device') {
onClose();
} else if (step === 'app-check') {
setStep('device');
} else if (step === 'location') {
setStep('app-check');
} else if (step === 'final') {
if (needsLocationSelection) {
setStep('location');
} else {
setStep('app-check');
}
}
}}
className="transition-colors"
style={{ color: 'var(--text-primary)' }}
>
{step === 'device' ? <X className="h-6 w-6" /> : <ChevronLeft className="h-6 w-6" />}
</button>
<h2 className="text-xl font-bold" style={{ color: 'var(--text-white)' }}>
{step === 'device' && '🎉 Подписка создана!'}
{step === 'app-check' && deviceType === 'desktop' && '💻 Настройка'}
{step === 'app-check' && deviceType === 'mobile' && '📱 Настройка'}
{step === 'location' && '🌍 Выбор локаций'}
{step === 'final' && '✨ Всё готово!'}
</h2>
<div className="w-6" />
</div>
{/* Progress bar */}
<div className="flex gap-2">
{Array.from({ length: totalSteps }).map((_, i) => (
<div
key={i}
className="h-1 flex-1 rounded-full transition-colors"
style={{
backgroundColor: i <= currentStepIndex ? 'var(--primary)' : 'var(--border)'
}}
/>
))}
</div>
</div>
{/* Content */}
<div className="p-6">
{/* Step 1: Device Selection */}
{step === 'device' && (
<div className="space-y-4">
<p className="text-center mb-6" style={{ color: 'var(--text-primary)' }}>
{expiryDate ? `Действует до ${expiryDate}` : 'Осталось 2 шага до подключения'}
</p>
<p className="font-medium text-center mb-4" style={{ color: 'var(--text-white)' }}>
Какое у вас устройство?
</p>
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => handleDeviceSelect('desktop')}
className="p-6 rounded-xl transition-all group border-2"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
>
<Laptop className="h-12 w-12 mx-auto mb-3 group-hover:scale-110 transition-transform" style={{ color: 'var(--primary)' }} />
<p className="font-medium" style={{ color: 'var(--text-white)' }}>Компьютер</p>
</button>
<button
onClick={() => handleDeviceSelect('mobile')}
className="p-6 rounded-xl transition-all group border-2"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
>
<Smartphone className="h-12 w-12 mx-auto mb-3 group-hover:scale-110 transition-transform" style={{ color: 'var(--primary)' }} />
<p className="font-medium" style={{ color: 'var(--text-white)' }}>Телефон</p>
</button>
</div>
</div>
)}
{/* Step 2: App Check */}
{step === 'app-check' && deviceType === 'desktop' && (
<div className="space-y-4">
<p className="text-white font-medium text-center mb-4">
У вас установлен Umbrix?
</p>
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => handleAppCheck(true)}
className="p-6 rounded-xl transition-all border-2"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
>
<Check className="h-12 w-12 mx-auto mb-3" style={{ color: 'var(--success)' }} />
<p className="font-medium" style={{ color: 'var(--text-white)' }}>Да</p>
</button>
<button
onClick={() => handleAppCheck(false)}
className="p-6 rounded-xl transition-all border-2"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
>
<X className="h-12 w-12 mx-auto mb-3" style={{ color: 'var(--error)' }} />
<p className="font-medium" style={{ color: 'var(--text-white)' }}>Нет</p>
</button>
</div>
{hasApp === false && (
<div className="mt-6 p-4 rounded-xl" style={{ background: 'rgba(47, 190, 165, 0.1)', borderWidth: '1px', borderStyle: 'solid', borderColor: 'var(--primary)' }}>
<p style={{ color: 'var(--text-white)' }} className="font-medium mb-3">Скачайте Umbrix:</p>
<div className="flex flex-col gap-2">
<a
href="https://umbrix.net/download"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 rounded-lg font-medium transition-colors text-center"
style={{ background: 'var(--primary)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--primary-dark)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--primary)'}
>
🪟 Windows
</a>
<a
href="https://umbrix.net/download"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 rounded-lg font-medium transition-colors text-center"
style={{ background: 'var(--primary)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--primary-dark)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--primary)'}
>
🍎 macOS
</a>
<a
href="https://umbrix.net/download"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 rounded-lg font-medium transition-colors text-center"
style={{ background: 'var(--primary)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--primary-dark)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--primary)'}
>
🐧 Linux
</a>
</div>
<button
onClick={() => setStep(needsLocationSelection ? 'location' : 'final')}
className="w-full mt-4 px-4 py-2 rounded-lg font-medium transition-colors"
style={{ background: 'var(--bg-elevated)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--border)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-elevated)'}
>
Уже установил, далее
</button>
</div>
)}
</div>
)}
{step === 'app-check' && deviceType === 'mobile' && (
<div className="space-y-4">
<p className="text-white font-medium text-center mb-4">
Какая у вас система?
</p>
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => {
setMobileOS('android');
setHasApp(null);
}}
className="p-6 rounded-xl border-2 transition-all"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--success)'}
onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}
>
<div className="text-4xl mb-3">🤖</div>
<p className="font-medium" style={{ color: 'var(--text-white)' }}>Android</p>
</button>
<button
onClick={() => {
setMobileOS('ios');
setHasApp(null);
}}
className="p-6 rounded-xl border-2 transition-all"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--text-white)'}
onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}
>
<div className="text-4xl mb-3">🍎</div>
<p className="font-medium" style={{ color: 'var(--text-white)' }}>iOS</p>
</button>
</div>
{mobileOS && (
<div className="mt-6">
<p className="text-white font-medium mb-3">У вас уже установлено приложение?</p>
<div className="grid grid-cols-2 gap-4 mb-4">
<button
onClick={() => handleAppCheck(true)}
className="px-4 py-3 bg-green-600 hover:bg-green-700 rounded-lg text-white font-medium transition-colors"
>
Да
</button>
<button
onClick={() => setHasApp(false)}
className="px-4 py-3 rounded-lg font-medium transition-colors"
style={{ background: 'var(--bg-elevated)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--border)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-elevated)'}
>
Нет
</button>
</div>
{hasApp === false && (
<div className="p-4 rounded-xl" style={{ background: 'rgba(47, 190, 165, 0.1)', borderWidth: '1px', borderStyle: 'solid', borderColor: 'var(--primary)' }}>
<p className="text-sm mb-2" style={{ color: 'var(--text-primary)' }}>Рекомендуем:</p>
{mobileOS === 'android' ? (
<div className="space-y-2">
<div style={{ color: 'var(--text-white)' }}> V2RayNG</div>
<div style={{ color: 'var(--text-white)' }}> Hiddify</div>
<div style={{ color: 'var(--text-white)' }}> v2rayTun</div>
</div>
) : (
<div className="space-y-2">
<div style={{ color: 'var(--text-white)' }}> Shadowrocket (AppStore)</div>
<div style={{ color: 'var(--text-white)' }}> Hiddify (AppStore)</div>
</div>
)}
<button
onClick={() => handleAppCheck(true)}
className="w-full mt-4 px-4 py-2 rounded-lg font-medium transition-colors"
style={{ background: 'var(--primary)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--primary-dark)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--primary)'}
>
Уже установил, далее
</button>
</div>
)}
</div>
)}
</div>
)}
{/* Step 3: Location Selection (только для extended тарифа) */}
{step === 'location' && (
<div className="space-y-4">
<p className="text-white font-medium text-center mb-2">
Выберите {maxLocations} локации
</p>
<p className="text-slate-400 text-sm text-center mb-4">
Выбрано: {selectedLocations.length} из {maxLocations}
</p>
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{locations.map((location) => {
const isSelected = selectedLocations.includes(location.id);
const canSelect = selectedLocations.length < maxLocations;
return (
<button
key={location.id}
onClick={() => handleLocationToggle(location.id)}
disabled={!isSelected && !canSelect}
className="w-full p-4 rounded-xl border-2 transition-all flex items-center justify-between"
style={{
background: isSelected ? 'rgba(47, 190, 165, 0.2)' : canSelect ? 'var(--bg-elevated)' : 'var(--bg-card)',
borderColor: isSelected ? 'var(--primary)' : 'var(--border)',
opacity: canSelect || isSelected ? 1 : 0.5,
cursor: canSelect || isSelected ? 'pointer' : 'not-allowed'
}}
onMouseEnter={e => { if (canSelect && !isSelected) e.currentTarget.style.borderColor = 'var(--text-primary)' }}
onMouseLeave={e => { if (!isSelected) e.currentTarget.style.borderColor = 'var(--border)' }}
>
<div className="flex items-center gap-3">
<div className="w-5 h-5 rounded border-2 flex items-center justify-center" style={{
background: isSelected ? 'var(--primary)' : 'transparent',
borderColor: isSelected ? 'var(--primary)' : 'var(--border)'
}}>
{isSelected && <Check className="h-4 w-4" style={{ color: 'var(--text-white)' }} />}
</div>
<span className="font-medium" style={{ color: 'var(--text-white)' }}>{location.name}</span>
</div>
<span className="text-sm" style={{ color: 'var(--text-primary)' }}>{location.ping}</span>
</button>
);
})}
</div>
<button
onClick={() => setStep('final')}
disabled={!canProceedFromLocation}
className="w-full py-3 rounded-xl font-medium transition-colors"
style={{
background: canProceedFromLocation ? 'var(--primary)' : 'var(--bg-elevated)',
color: canProceedFromLocation ? 'var(--text-white)' : 'var(--text-primary)',
cursor: canProceedFromLocation ? 'pointer' : 'not-allowed',
opacity: canProceedFromLocation ? 1 : 0.5
}}
onMouseEnter={e => { if (canProceedFromLocation) e.currentTarget.style.background = 'var(--primary-dark)' }}
onMouseLeave={e => { if (canProceedFromLocation) e.currentTarget.style.background = 'var(--primary)' }}
>
{canProceedFromLocation ? '➡️ Продолжить' : `Выберите ещё ${maxLocations - selectedLocations.length}`}
</button>
</div>
)}
{/* Step 4: Final - Show Link/QR */}
{step === 'final' && (
<div className="space-y-4">
<div className="text-center mb-6">
<div className="text-4xl mb-3">🎉</div>
<p className="text-sm" style={{ color: 'var(--text-primary)' }}>
{deviceType === 'desktop' ? 'Скопируйте ссылку и вставьте в Umbrix' : 'Отсканируйте QR код или скопируйте ссылку'}
</p>
</div>
<button
onClick={handleCopy}
className="w-full p-4 rounded-xl border-2 transition-all group"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--primary)'}
onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}
>
<div className="flex items-center justify-between">
<span className="font-medium" style={{ color: 'var(--text-white)' }}>
{copied ? '✅ Скопировано!' : '📋 Скопировать ссылку'}
</span>
<Copy className="h-5 w-5 transition-colors" style={{ color: copied ? 'var(--success)' : 'var(--text-primary)' }} />
</div>
</button>
{deviceType === 'mobile' && (
<button
onClick={() => setShowQR(!showQR)}
className="w-full p-4 rounded-xl border-2 transition-all group"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--primary)'}
onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}
>
<div className="flex items-center justify-between">
<span className="font-medium" style={{ color: 'var(--text-white)' }}>
{showQR ? '📱 Скрыть QR код' : '📱 Показать QR код'}
</span>
<QrCode className="h-5 w-5 transition-colors" style={{ color: 'var(--text-primary)' }} />
</div>
</button>
)}
{showQR && (
<div className="p-6 bg-white rounded-xl">
<div className="text-center text-slate-800 text-sm mb-2">QR код</div>
<div className="text-xs text-slate-600 break-all">{subscriptionUrl}</div>
</div>
)}
<div className="pt-4 mt-4" style={{ borderTop: '1px solid var(--border)' }}>
<button
onClick={onClose}
className="w-full p-4 rounded-xl transition-colors font-medium"
style={{ background: 'var(--bg-elevated)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--border)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-elevated)'}
>
📖 Нужна помощь? Открыть инструкцию
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
}