diff --git a/src/components/dashboard/Sparkline.tsx b/src/components/dashboard/Sparkline.tsx index e85e948..54da5b7 100644 --- a/src/components/dashboard/Sparkline.tsx +++ b/src/components/dashboard/Sparkline.tsx @@ -4,36 +4,33 @@ interface SparklineProps { data: number[]; width?: number; height?: number; + color: string; className?: string; } export default function Sparkline({ data, - width = 120, - height = 32, + width = 200, + height = 40, + color, className = '', }: SparklineProps) { const gradientId = useId(); if (data.length < 2) return null; - const max = Math.max(...data); - const min = Math.min(...data); - const range = max - min || 1; - const padding = 2; - const innerW = width - padding * 2; - const innerH = height - padding * 2; - + const max = Math.max(...data, 1); const points = data.map((v, i) => { - const x = padding + (i / (data.length - 1)) * innerW; - const y = padding + innerH - ((v - min) / range) * innerH; + const x = (i / (data.length - 1)) * width; + const y = height - (v / max) * height * 0.85 - 2; return `${x},${y}`; }); const polyline = points.join(' '); - const lastX = padding + innerW; - const bottomY = padding + innerH; - const areaPath = `M${points[0]} ${points.slice(1).join(' ')} L${lastX},${bottomY} L${padding},${bottomY} Z`; + const lastPoint = points[points.length - 1].split(','); + const lastX = parseFloat(lastPoint[0]); + const lastY = parseFloat(lastPoint[1]); + const areaPath = `M${points[0]} ${points.slice(1).join(' ')} L${width},${height} L0,${height} Z`; return ( ); } diff --git a/src/components/dashboard/StatsGrid.tsx b/src/components/dashboard/StatsGrid.tsx index 5e81413..d176595 100644 --- a/src/components/dashboard/StatsGrid.tsx +++ b/src/components/dashboard/StatsGrid.tsx @@ -1,7 +1,9 @@ +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router'; import type { Subscription } from '../../types'; import { useCurrency } from '../../hooks/useCurrency'; +import { getTrafficZone } from '../../utils/trafficZone'; interface StatsGridProps { balanceRubles: number; @@ -12,83 +14,21 @@ interface StatsGridProps { refLoading: boolean; } -const ArrowRightIcon = () => ( +const ChevronIcon = () => ( -); - -const WalletIcon = () => ( - -); - -const CalendarIcon = () => ( - -); - -const UsersIcon = () => ( - -); - -const CoinIcon = () => ( - ); @@ -104,110 +44,166 @@ export default function StatsGrid({ const { t } = useTranslation(); const { formatAmount, currencySymbol, formatPositive } = useCurrency(); + const zone = useMemo( + () => getTrafficZone(subscription?.traffic_used_percent ?? 0), + [subscription?.traffic_used_percent], + ); + + const cards = [ + { + label: t('dashboard.stats.balance'), + value: `${formatAmount(balanceRubles)} ${currencySymbol}`, + valueColor: zone.mainHex, + to: '/balance', + icon: (color: string) => ( + + ), + iconBg: `${zone.mainHex}12`, + iconColor: zone.mainHex, + loading: false, + onboarding: 'balance', + }, + { + label: t('dashboard.stats.subscription'), + value: subscription ? `${subscription.days_left}` : '—', + valueSuffix: subscription ? ` ${t('subscription.daysShort')}` : '', + valueColor: '#fff', + to: '/subscription', + icon: (color: string) => ( + + ), + iconBg: 'rgba(255,255,255,0.06)', + iconColor: 'rgba(255,255,255,0.45)', + loading: subLoading, + onboarding: 'subscription-status', + }, + { + label: t('dashboard.stats.referrals'), + value: `${referralCount}`, + valueColor: '#fff', + to: '/referral', + icon: (color: string) => ( + + ), + iconBg: 'rgba(255,255,255,0.06)', + iconColor: 'rgba(255,255,255,0.45)', + loading: refLoading, + }, + { + label: t('dashboard.stats.earnings'), + value: formatPositive(earningsRubles), + valueColor: zone.mainHex, + to: '/referral', + icon: (color: string) => ( + + ), + iconBg: `${zone.mainHex}12`, + iconColor: zone.mainHex, + loading: refLoading, + }, + ]; + return ( -
- {/* Balance */} - -
-
- +
+ {cards.map((card, i) => ( + + {/* Top row: icon + label + arrow */} +
+
+
+ {card.icon(card.iconColor)} +
+ {card.label} +
+
- - - -
-
- {t('dashboard.stats.balance')} -
-
- {formatAmount(balanceRubles)} - {currencySymbol} -
- - {/* Subscription Days */} - -
-
- -
- - - -
-
- {t('dashboard.stats.subscription')} -
- {subLoading ? ( -
- ) : subscription ? ( -
- {subscription.days_left > 0 ? ( - <> - {subscription.days_left} - {t('subscription.days')} - - ) : subscription.hours_left > 0 ? ( - <> - {subscription.hours_left} - {t('subscription.hours')} - - ) : ( - {t('subscription.expired')} - )} -
- ) : ( -
- {t('subscription.inactive')} -
- )} - - - {/* Referrals */} - -
-
- -
- - - -
-
- {t('dashboard.stats.referrals')} -
- {refLoading ? ( -
- ) : ( -
{referralCount}
- )} - - - {/* Earnings */} - -
-
- -
- - - -
-
- {t('dashboard.stats.earnings')} -
- {refLoading ? ( -
- ) : ( -
- {formatPositive(earningsRubles)} -
- )} - + {/* Value */} + {card.loading ? ( +
+ ) : ( +
+ {card.value} + {card.valueSuffix && ( + + {card.valueSuffix} + + )} +
+ )} + + ))}
); } diff --git a/src/components/dashboard/SubscriptionCardActive.tsx b/src/components/dashboard/SubscriptionCardActive.tsx index 0e02eb9..b306621 100644 --- a/src/components/dashboard/SubscriptionCardActive.tsx +++ b/src/components/dashboard/SubscriptionCardActive.tsx @@ -1,10 +1,13 @@ +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Link, useNavigate } from 'react-router'; +import { useNavigate } from 'react-router'; +import { Link } from 'react-router'; import { UseMutationResult } from '@tanstack/react-query'; -import { HoverBorderGradient } from '../ui/hover-border-gradient'; import TrafficProgressBar from './TrafficProgressBar'; +import Sparkline from './Sparkline'; import { useAnimatedNumber } from '../../hooks/useAnimatedNumber'; import { getTrafficZone } from '../../utils/trafficZone'; +import { formatTraffic } from '../../utils/formatTraffic'; import type { Subscription } from '../../types'; interface SubscriptionCardActiveProps { @@ -35,23 +38,6 @@ const RefreshIcon = ({ className = 'w-4 h-4' }: { className?: string }) => ( ); -const DeviceIcon = () => ( - -); - export default function SubscriptionCardActive({ subscription, trafficData, @@ -64,128 +50,318 @@ export default function SubscriptionCardActive({ const usedPercent = trafficData?.traffic_used_percent ?? subscription.traffic_used_percent; const usedGb = trafficData?.traffic_used_gb ?? subscription.traffic_used_gb; const isUnlimited = trafficData?.is_unlimited ?? subscription.traffic_limit_gb === 0; - const zone = getTrafficZone(usedPercent); + const zone = useMemo(() => getTrafficZone(usedPercent), [usedPercent]); const animatedPercent = useAnimatedNumber(usedPercent); const formattedDate = new Date(subscription.end_date).toLocaleDateString(); + const daysLeft = subscription.days_left; + + // Sparkline placeholder data (hidden until API provides daily usage) + const dailyUsage: number[] = []; return (
- {/* Top row: zone indicator + tariff info */} -
-
- {/* Animated zone dot */} -