Add files via upload

This commit is contained in:
Egor
2026-01-21 09:08:08 +03:00
committed by GitHub
parent 94a8671d9e
commit 5709ab4ff1
2 changed files with 114 additions and 9 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from 'react' import { useState, useEffect, useMemo, useRef } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -145,18 +145,19 @@ export default function Dashboard() {
traffic_used_percent: data.traffic_used_percent, traffic_used_percent: data.traffic_used_percent,
is_unlimited: data.is_unlimited, is_unlimited: data.is_unlimited,
}) })
// Save last refresh timestamp to localStorage
localStorage.setItem('traffic_refresh_ts', Date.now().toString())
if (data.rate_limited && data.retry_after_seconds) { if (data.rate_limited && data.retry_after_seconds) {
setTrafficRefreshCooldown(data.retry_after_seconds) setTrafficRefreshCooldown(data.retry_after_seconds)
} else { } else {
setTrafficRefreshCooldown(60) setTrafficRefreshCooldown(30)
} }
// Also update subscription query cache
queryClient.invalidateQueries({ queryKey: ['subscription'] }) queryClient.invalidateQueries({ queryKey: ['subscription'] })
}, },
onError: (error: { response?: { status?: number; headers?: { get?: (key: string) => string } } }) => { onError: (error: { response?: { status?: number; headers?: { get?: (key: string) => string } } }) => {
if (error.response?.status === 429) { if (error.response?.status === 429) {
const retryAfter = error.response.headers?.get?.('Retry-After') const retryAfter = error.response.headers?.get?.('Retry-After')
setTrafficRefreshCooldown(retryAfter ? parseInt(retryAfter, 10) : 60) setTrafficRefreshCooldown(retryAfter ? parseInt(retryAfter, 10) : 30)
} }
}, },
}) })
@@ -170,6 +171,31 @@ export default function Dashboard() {
return () => clearInterval(timer) return () => clearInterval(timer)
}, [trafficRefreshCooldown]) }, [trafficRefreshCooldown])
// Track if we've already triggered auto-refresh this session
const hasAutoRefreshed = useRef(false)
// Auto-refresh traffic on mount (with 30s caching)
useEffect(() => {
if (!subscription) return
if (hasAutoRefreshed.current) return
hasAutoRefreshed.current = true
const lastRefresh = localStorage.getItem('traffic_refresh_ts')
const now = Date.now()
const cacheMs = 30 * 1000
if (lastRefresh && now - parseInt(lastRefresh, 10) < cacheMs) {
const elapsed = now - parseInt(lastRefresh, 10)
const remaining = Math.ceil((cacheMs - elapsed) / 1000)
if (remaining > 0) {
setTrafficRefreshCooldown(remaining)
}
return
}
refreshTrafficMutation.mutate()
}, [subscription])
const hasNoSubscription = !subscription && !subLoading && subError const hasNoSubscription = !subscription && !subLoading && subError
// Show onboarding for new users after data loads // Show onboarding for new users after data loads

View File

@@ -100,6 +100,14 @@ export default function Subscription() {
const [showServerManagement, setShowServerManagement] = useState(false) const [showServerManagement, setShowServerManagement] = useState(false)
const [selectedServersToUpdate, setSelectedServersToUpdate] = useState<string[]>([]) const [selectedServersToUpdate, setSelectedServersToUpdate] = useState<string[]>([])
// Traffic refresh state
const [trafficRefreshCooldown, setTrafficRefreshCooldown] = useState(0)
const [trafficData, setTrafficData] = useState<{
traffic_used_gb: number
traffic_used_percent: number
is_unlimited: boolean
} | null>(null)
const { data: subscription, isLoading } = useQuery({ const { data: subscription, isLoading } = useQuery({
queryKey: ['subscription'], queryKey: ['subscription'],
queryFn: subscriptionApi.getSubscription, queryFn: subscriptionApi.getSubscription,
@@ -326,6 +334,65 @@ export default function Subscription() {
}, },
}) })
// Traffic refresh mutation
const refreshTrafficMutation = useMutation({
mutationFn: subscriptionApi.refreshTraffic,
onSuccess: (data) => {
setTrafficData({
traffic_used_gb: data.traffic_used_gb,
traffic_used_percent: data.traffic_used_percent,
is_unlimited: data.is_unlimited,
})
localStorage.setItem('traffic_refresh_ts', Date.now().toString())
if (data.rate_limited && data.retry_after_seconds) {
setTrafficRefreshCooldown(data.retry_after_seconds)
} else {
setTrafficRefreshCooldown(30)
}
queryClient.invalidateQueries({ queryKey: ['subscription'] })
},
onError: (error: { response?: { status?: number; headers?: { get?: (key: string) => string } } }) => {
if (error.response?.status === 429) {
const retryAfter = error.response.headers?.get?.('Retry-After')
setTrafficRefreshCooldown(retryAfter ? parseInt(retryAfter, 10) : 30)
}
},
})
// Track if we've already triggered auto-refresh this session
const hasAutoRefreshed = useRef(false)
// Cooldown timer for traffic refresh
useEffect(() => {
if (trafficRefreshCooldown <= 0) return
const timer = setInterval(() => {
setTrafficRefreshCooldown((prev) => Math.max(0, prev - 1))
}, 1000)
return () => clearInterval(timer)
}, [trafficRefreshCooldown])
// Auto-refresh traffic on mount (with 30s caching)
useEffect(() => {
if (!subscription) return
if (hasAutoRefreshed.current) return
hasAutoRefreshed.current = true
const lastRefresh = localStorage.getItem('traffic_refresh_ts')
const now = Date.now()
const cacheMs = 30 * 1000
if (lastRefresh && now - parseInt(lastRefresh, 10) < cacheMs) {
const elapsed = now - parseInt(lastRefresh, 10)
const remaining = Math.ceil((cacheMs - elapsed) / 1000)
if (remaining > 0) {
setTrafficRefreshCooldown(remaining)
}
return
}
refreshTrafficMutation.mutate()
}, [subscription])
// Auto-scroll to switch tariff modal when it appears // Auto-scroll to switch tariff modal when it appears
useEffect(() => { useEffect(() => {
if (switchTariffId && switchModalRef.current) { if (switchTariffId && switchModalRef.current) {
@@ -495,9 +562,21 @@ export default function Subscription() {
</div> </div>
</div> </div>
<div> <div>
<div className="text-sm text-dark-500 mb-1">{t('subscription.traffic')}</div> <div className="flex items-center gap-2 mb-1">
<span className="text-sm text-dark-500">{t('subscription.traffic')}</span>
<button
onClick={() => refreshTrafficMutation.mutate()}
disabled={refreshTrafficMutation.isPending || trafficRefreshCooldown > 0}
className="p-1 rounded-full hover:bg-dark-700/50 text-dark-400 hover:text-accent-400 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title={trafficRefreshCooldown > 0 ? `${trafficRefreshCooldown}s` : t('common.refresh')}
>
<svg className={`w-3.5 h-3.5 ${refreshTrafficMutation.isPending ? 'animate-spin' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
</div>
<div className="text-xl font-semibold text-dark-100"> <div className="text-xl font-semibold text-dark-100">
{subscription.traffic_used_gb.toFixed(1)} / {subscription.traffic_limit_gb || '∞'} GB {(trafficData?.traffic_used_gb ?? subscription.traffic_used_gb).toFixed(1)} / {subscription.traffic_limit_gb || '∞'} GB
</div> </div>
</div> </div>
<div> <div>
@@ -531,12 +610,12 @@ export default function Subscription() {
<div className="mb-6"> <div className="mb-6">
<div className="flex justify-between text-sm mb-2"> <div className="flex justify-between text-sm mb-2">
<span className="text-dark-400">{t('subscription.trafficUsed')}</span> <span className="text-dark-400">{t('subscription.trafficUsed')}</span>
<span className="text-dark-300">{subscription.traffic_used_percent.toFixed(1)}%</span> <span className="text-dark-300">{(trafficData?.traffic_used_percent ?? subscription.traffic_used_percent).toFixed(1)}%</span>
</div> </div>
<div className="progress-bar"> <div className="progress-bar">
<div <div
className={`progress-fill ${getTrafficColor(subscription.traffic_used_percent)}`} className={`progress-fill ${getTrafficColor(trafficData?.traffic_used_percent ?? subscription.traffic_used_percent)}`}
style={{ width: `${Math.min(subscription.traffic_used_percent, 100)}%` }} style={{ width: `${Math.min(trafficData?.traffic_used_percent ?? subscription.traffic_used_percent, 100)}%` }}
/> />
</div> </div>
</div> </div>