From 26c4239affad9e4f6b812b4e6d635669e82e9cbd Mon Sep 17 00:00:00 2001 From: Umbrix Dev Date: Wed, 4 Feb 2026 05:21:14 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=97=20Page:=20=D0=9F=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D0=BC=D0=BE=D1=82=D1=80=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD?= =?UTF-8?q?=D1=83=20(=D0=B2=D0=BD=D0=B5=D1=88=D0=BD=D0=B8=D0=B9=20=D0=B4?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=83=D0=BF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/subscription/[token]/page.tsx | 308 ++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 app/subscription/[token]/page.tsx diff --git a/app/subscription/[token]/page.tsx b/app/subscription/[token]/page.tsx new file mode 100644 index 0000000..4bd8bf2 --- /dev/null +++ b/app/subscription/[token]/page.tsx @@ -0,0 +1,308 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { marzbanApi, type MarzbanUser } from '@/lib/marzban-api'; +import { + Copy, ExternalLink, Database, Calendar, TrendingUp, + ChevronLeft, AlertCircle, Check +} from 'lucide-react'; + +// Haptic feedback helper +const haptic = { + impact: (style?: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft') => { + if (typeof window !== 'undefined') { + const tg = (window as any).Telegram?.WebApp; + if (tg?.HapticFeedback?.impactOccurred) { + tg.HapticFeedback.impactOccurred(style || 'medium'); + } + } + }, + notification: (type: 'success' | 'warning' | 'error') => { + if (typeof window !== 'undefined') { + const tg = (window as any).Telegram?.WebApp; + if (tg?.HapticFeedback?.notificationOccurred) { + tg.HapticFeedback.notificationOccurred(type); + } + } + }, + selection: () => { + if (typeof window !== 'undefined') { + const tg = (window as any).Telegram?.WebApp; + if (tg?.HapticFeedback?.selectionChanged) { + tg.HapticFeedback.selectionChanged(); + } + } + } +}; + +export default function SubscriptionPage() { + const params = useParams(); + const router = useRouter(); + const token = params.token as string; + + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (!token) { + setError('Токен не найден'); + setLoading(false); + return; + } + + fetchUserData(); + }, [token]); + + const fetchUserData = async () => { + setLoading(true); + setError(null); + + try { + // ✅ Используем новый Marzban API (с fallback на HTML scraping) + const userData = await marzbanApi.getUserInfo(token); + setUser(userData); + } catch (err: any) { + console.error('Failed to fetch user data:', err); + setError(err.message || 'Не удалось загрузить данные подписки'); + } finally { + setLoading(false); + } + }; + + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + setCopied(true); + haptic?.notification('success'); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy:', err); + haptic?.notification('error'); + } + }; + + const openInApp = () => { + haptic?.impact('medium'); + const subscriptionUrl = `https://umbrix2.3to3.sbs/sub/${token}/`; + + // Пробуем открыть через deep link + const deepLink = `umbrix://import/${subscriptionUrl}`; + + // Создаем невидимый iframe для попытки открыть приложение + const iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.src = deepLink; + document.body.appendChild(iframe); + + // Через 2 секунды проверяем - если приложение не открылось, показываем страницу установки + setTimeout(() => { + document.body.removeChild(iframe); + + // Если всё ещё на странице (приложение не открылось), перенаправляем на /setup + if (document.hasFocus()) { + router.push('/setup'); + } + }, 2000); + }; + + if (loading) { + return ( +
+
+
+

Загрузка данных...

+
+
+ ); + } + + if (error || !user) { + return ( +
+
+
+ +

Ошибка загрузки

+

{error || 'Подписка не найдена'}

+ +
+
+
+ ); + } + + const usedTrafficGB = user.used_traffic / (1024 * 1024 * 1024); + const totalTrafficGB = user.data_limit ? user.data_limit / (1024 * 1024 * 1024) : null; + const trafficPercent = totalTrafficGB ? (usedTrafficGB / totalTrafficGB) * 100 : 0; + const daysRemaining = marzbanApi.getDaysUntilExpire(user.expire); + + return ( +
+ {/* Header with Back Button */} +
+
+ +

Моя подписка

+
+
+ + {/* Main Content */} +
+ {/* User Info Card */} +
+
+
+

{user.username}

+ + {marzbanApi.getStatusText(user.status)} + +
+
+ + {/* Stats Grid */} +
+ {/* Traffic Usage */} +
+
+ + + Использовано трафика + + + {usedTrafficGB.toFixed(2)} GB + {totalTrafficGB && ` / ${totalTrafficGB.toFixed(0)} GB`} + +
+ + {/* Progress Bar */} + {totalTrafficGB && ( +
+
90 + ? 'bg-red-500' + : trafficPercent > 70 + ? 'bg-yellow-500' + : 'bg-[var(--primary)]' + }`} + style={{ width: `${Math.min(trafficPercent, 100)}%` }} + >
+
+ )} + + {totalTrafficGB && ( +

+ Осталось: {(totalTrafficGB - usedTrafficGB).toFixed(2)} GB ( + {(100 - trafficPercent).toFixed(1)}%) +

+ )} +
+ + {/* Expiry Date */} +
+ + + Действует до + +
+

{marzbanApi.formatExpireDate(user.expire)}

+ {daysRemaining !== null && daysRemaining <= 30 && ( +

+ Осталось {daysRemaining} {daysRemaining === 1 ? 'день' : 'дней'} +

+ )} +
+
+ + {/* Last Updated */} +
+ + + Обновлено + + + {new Date(user.sub_updated_at).toLocaleString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} + +
+
+
+ + {/* Quick Actions */} +
+ + + +
+ + {/* Info Card */} +
+

+ 💡 Скопируйте ссылку и вставьте её в приложение Umbrix для подключения к VPN +

+
+ + {/* Refresh Button */} + +
+
+ ); +}