mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
@@ -321,4 +321,24 @@ export const subscriptionApi = {
|
|||||||
const response = await apiClient.put('/cabinet/subscription/traffic', { gb })
|
const response = await apiClient.put('/cabinet/subscription/traffic', { gb })
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Refresh traffic usage from RemnaWave (rate limited: 1 per 60 seconds)
|
||||||
|
refreshTraffic: async (): Promise<{
|
||||||
|
success: boolean
|
||||||
|
cached: boolean
|
||||||
|
rate_limited?: boolean
|
||||||
|
retry_after_seconds?: number
|
||||||
|
source?: string
|
||||||
|
traffic_used_bytes: number
|
||||||
|
traffic_used_gb: number
|
||||||
|
traffic_limit_bytes: number
|
||||||
|
traffic_limit_gb: number
|
||||||
|
traffic_used_percent: number
|
||||||
|
is_unlimited: boolean
|
||||||
|
lifetime_used_bytes?: number
|
||||||
|
lifetime_used_gb?: number
|
||||||
|
}> => {
|
||||||
|
const response = await apiClient.post('/cabinet/subscription/refresh-traffic')
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ const ChevronRightIcon = () => (
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const RefreshIcon = ({ className = "w-4 h-4" }: { className?: string }) => (
|
||||||
|
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
// Check if device might be low-performance (Telegram WebApp on mobile)
|
// Check if device might be low-performance (Telegram WebApp on mobile)
|
||||||
const isLowPerfDevice = (() => {
|
const isLowPerfDevice = (() => {
|
||||||
const isTelegramWebApp = !!(window as unknown as { Telegram?: { WebApp?: unknown } }).Telegram?.WebApp
|
const isTelegramWebApp = !!(window as unknown as { Telegram?: { WebApp?: unknown } }).Telegram?.WebApp
|
||||||
@@ -123,6 +129,47 @@ export default function Dashboard() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Traffic refresh state and mutation
|
||||||
|
const [trafficRefreshCooldown, setTrafficRefreshCooldown] = useState(0)
|
||||||
|
const [trafficData, setTrafficData] = useState<{
|
||||||
|
traffic_used_gb: number
|
||||||
|
traffic_used_percent: number
|
||||||
|
is_unlimited: boolean
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
if (data.rate_limited && data.retry_after_seconds) {
|
||||||
|
setTrafficRefreshCooldown(data.retry_after_seconds)
|
||||||
|
} else {
|
||||||
|
setTrafficRefreshCooldown(60)
|
||||||
|
}
|
||||||
|
// Also update subscription query cache
|
||||||
|
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) : 60)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cooldown timer
|
||||||
|
useEffect(() => {
|
||||||
|
if (trafficRefreshCooldown <= 0) return
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setTrafficRefreshCooldown((prev) => Math.max(0, prev - 1))
|
||||||
|
}, 1000)
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [trafficRefreshCooldown])
|
||||||
|
|
||||||
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
|
||||||
@@ -223,9 +270,19 @@ export default function Dashboard() {
|
|||||||
</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')}
|
||||||
|
>
|
||||||
|
<RefreshIcon className={`w-3.5 h-3.5 ${refreshTrafficMutation.isPending ? 'animate-spin' : ''}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="text-dark-100 font-medium">
|
<div className="text-dark-100 font-medium">
|
||||||
{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>
|
||||||
@@ -248,12 +305,14 @@ export default function Dashboard() {
|
|||||||
<div className="mt-6">
|
<div className="mt-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>
|
||||||
|
|||||||
Reference in New Issue
Block a user