From f474067efbb36974b47b51ba568304b6cd6b3805 Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 25 Feb 2026 11:00:14 +0300 Subject: [PATCH] feat: adapt dashboard and subscription page for light theme - Add glassTheme utility with theme-aware color tokens - Replace hardcoded rgba(255,255,255,...) with dynamic values - Replace text-white with text-dark-50 for auto theme remapping - Restyle subscription page block to match dashboard glassmorphic design - Light mode: white semi-opaque cards with subtle shadows - Dark mode: unchanged visual appearance --- src/components/dashboard/StatsGrid.tsx | 34 +- .../dashboard/SubscriptionCardActive.tsx | 71 +- .../dashboard/SubscriptionCardExpired.tsx | 22 +- .../dashboard/TrafficProgressBar.tsx | 16 +- src/components/dashboard/TrialOfferCard.tsx | 24 +- src/pages/Subscription.tsx | 1189 +++++++++++------ src/utils/glassTheme.ts | 43 + 7 files changed, 921 insertions(+), 478 deletions(-) create mode 100644 src/utils/glassTheme.ts diff --git a/src/components/dashboard/StatsGrid.tsx b/src/components/dashboard/StatsGrid.tsx index d176595..55ba036 100644 --- a/src/components/dashboard/StatsGrid.tsx +++ b/src/components/dashboard/StatsGrid.tsx @@ -3,7 +3,9 @@ import { useTranslation } from 'react-i18next'; import { Link } from 'react-router'; import type { Subscription } from '../../types'; import { useCurrency } from '../../hooks/useCurrency'; +import { useTheme } from '../../hooks/useTheme'; import { getTrafficZone } from '../../utils/trafficZone'; +import { getGlassColors } from '../../utils/glassTheme'; interface StatsGridProps { balanceRubles: number; @@ -14,18 +16,18 @@ interface StatsGridProps { refLoading: boolean; } -const ChevronIcon = () => ( +const ChevronIcon = ({ color }: { color: string }) => ( ), - iconBg: 'rgba(255,255,255,0.06)', - iconColor: 'rgba(255,255,255,0.45)', + iconBg: g.trackBg, + iconColor: g.textSecondary, loading: subLoading, onboarding: 'subscription-status', }, { label: t('dashboard.stats.referrals'), value: `${referralCount}`, - valueColor: '#fff', + valueColor: g.text, to: '/referral', icon: (color: string) => ( ), - iconBg: 'rgba(255,255,255,0.06)', - iconColor: 'rgba(255,255,255,0.45)', + iconBg: g.trackBg, + iconColor: g.textSecondary, loading: refLoading, }, { @@ -165,9 +169,9 @@ export default function StatsGrid({ to={card.to} className="group relative overflow-hidden rounded-[18px] transition-all duration-200" style={{ - background: - 'linear-gradient(145deg, rgba(255,255,255,0.045) 0%, rgba(255,255,255,0.02) 100%)', - border: '1px solid rgba(255,255,255,0.06)', + background: g.cardBg, + border: `1px solid ${g.cardBorder}`, + boxShadow: g.shadow, padding: '18px 20px 20px', }} data-onboarding={card.onboarding} @@ -181,9 +185,9 @@ export default function StatsGrid({ > {card.icon(card.iconColor)} - {card.label} + {card.label} - + {/* Value */} @@ -196,7 +200,7 @@ export default function StatsGrid({ > {card.value} {card.valueSuffix && ( - + {card.valueSuffix} )} diff --git a/src/components/dashboard/SubscriptionCardActive.tsx b/src/components/dashboard/SubscriptionCardActive.tsx index dfd00d3..0ca4e8c 100644 --- a/src/components/dashboard/SubscriptionCardActive.tsx +++ b/src/components/dashboard/SubscriptionCardActive.tsx @@ -6,8 +6,10 @@ import { UseMutationResult } from '@tanstack/react-query'; import TrafficProgressBar from './TrafficProgressBar'; import Sparkline from './Sparkline'; import { useAnimatedNumber } from '../../hooks/useAnimatedNumber'; +import { useTheme } from '../../hooks/useTheme'; import { getTrafficZone } from '../../utils/trafficZone'; import { formatTraffic } from '../../utils/formatTraffic'; +import { getGlassColors } from '../../utils/glassTheme'; import type { Subscription } from '../../types'; interface SubscriptionCardActiveProps { @@ -48,6 +50,8 @@ export default function SubscriptionCardActive({ }: SubscriptionCardActiveProps) { const { t } = useTranslation(); const navigate = useNavigate(); + const { isDark } = useTheme(); + const g = getGlassColors(isDark); const usedPercent = trafficData?.traffic_used_percent ?? subscription.traffic_used_percent; const usedGb = trafficData?.traffic_used_gb ?? subscription.traffic_used_gb; @@ -65,12 +69,12 @@ export default function SubscriptionCardActive({
{/* Trial shimmer border */} @@ -90,7 +94,7 @@ export default function SubscriptionCardActive({ width: 200, height: 200, borderRadius: '50%', - background: `radial-gradient(circle, ${zone.mainHex}15 0%, transparent 70%)`, + background: `radial-gradient(circle, ${zone.mainHex}${g.glowAlpha} 0%, transparent 70%)`, transition: 'background 0.8s ease', }} aria-hidden="true" @@ -147,7 +151,7 @@ export default function SubscriptionCardActive({
{/* Title */} -

+

{t('dashboard.trafficUsageTitle')}

@@ -162,17 +166,17 @@ export default function SubscriptionCardActive({ > ∞ -
+
{formatTraffic(usedGb)} {t('dashboard.usedSuffix')}
) : ( <> -
+
{animatedPercent.toFixed(0)} - % + %
-
+
{formatTraffic(usedGb)} / {formatTraffic(subscription.traffic_limit_gb)}
@@ -194,9 +198,13 @@ export default function SubscriptionCardActive({ {subscription.subscription_url && ( +
+
+ {subscription.traffic_reset_mode && + subscription.traffic_reset_mode !== 'NO_RESET' && ( +
+ {t(`subscription.trafficReset.${subscription.traffic_reset_mode}`)} +
+ )}
- {/* Subscription URL - hidden when hide_subscription_link is true */} - {!subscription.hide_subscription_link && ( -
- + {/* ─── Connect Device Button ─── */} + {subscription.subscription_url && ( + + )} + + {/* ─── Subscription URL ─── */} + {subscription.subscription_url && !subscription.hide_subscription_link && ( +
+ {subscription.subscription_url}
)} -
- )} -
-
-
{t('subscription.daysLeft')}
-
- {subscription.days_left > 0 ? ( - t('subscription.days', { count: subscription.days_left }) - ) : subscription.hours_left > 0 ? ( - `${t('subscription.hours', { count: subscription.hours_left })} ${t('subscription.minutes', { count: subscription.minutes_left })}` - ) : subscription.minutes_left > 0 ? ( - t('subscription.minutes', { count: subscription.minutes_left }) - ) : ( - {t('subscription.expired')} - )} -
-
-
-
{t('subscription.expiresAt')}
-
- {new Date(subscription.end_date).toLocaleDateString()} -
-
-
-
- {t('subscription.traffic')} - -
-
- {(trafficData?.traffic_used_gb ?? subscription.traffic_used_gb).toFixed(1)} /{' '} - {subscription.traffic_limit_gb || '∞'} GB -
- {subscription.traffic_reset_mode && - subscription.traffic_reset_mode !== 'NO_RESET' && ( -
- {t(`subscription.trafficReset.${subscription.traffic_reset_mode}`)} +
+
+ +
+ {t('dashboard.remaining')}
- )} -
-
-
{t('subscription.devices')}
-
{subscription.device_limit}
-
-
+
+ + {daysLeft > 0 + ? daysLeft + : subscription.hours_left > 0 + ? `${subscription.hours_left}h` + : `${subscription.minutes_left}m`} + + + {daysLeft > 0 ? t('subscription.daysShort') : ''} + +
+
- {/* Locations */} - {subscription.servers && subscription.servers.length > 0 && ( -
-
{t('subscription.locationsLabel')}
-
- {subscription.servers.map((server) => ( - - {server.country_code && ( - {getFlagEmoji(server.country_code)} - )} - {server.name} - - ))} -
-
- )} + {/* Devices */} +
+
+
+ +
+ {t('subscription.devices')} +
+
+ + {connectedDevices} + + + / {subscription.device_limit} + +
+
- {/* Traffic Usage */} - {subscription.traffic_limit_gb > 0 && ( -
-
- {t('subscription.trafficUsed')} - - {(trafficData?.traffic_used_percent ?? subscription.traffic_used_percent).toFixed( - 1, - )} - % - -
- -
- )} + {/* Expires */} +
+
+ {t('subscription.expiresAt')} +
+
+ {formattedDate} +
+
- {/* Purchased Traffic Packages */} - {subscription.traffic_purchases && subscription.traffic_purchases.length > 0 && ( -
-
{t('subscription.purchasedTraffic')}
-
- {subscription.traffic_purchases.map((purchase) => ( + {/* Traffic */} +
-
-
- - - - - {purchase.traffic_gb} {t('common.units.gb')} - -
-
-
- {purchase.days_remaining === 0 ? ( - {t('subscription.expired')} - ) : ( - - {t('subscription.days', { count: purchase.days_remaining })} - - )} -
-
- - - - - {t('subscription.trafficResetAt')}:{' '} - {new Date(purchase.expires_at).toLocaleDateString(undefined, { - day: '2-digit', - month: '2-digit', - year: 'numeric', - })} - -
-
-
-
-
-
-
- {new Date(purchase.created_at).toLocaleDateString()} - {new Date(purchase.expires_at).toLocaleDateString()} -
+ {t('subscription.traffic')} +
+
+ {isUnlimited ? '∞' : `${usedPercent.toFixed(0)}%`}
- ))} -
-
- )} - - {/* Autopay Toggle - hide for daily tariffs */} - {!subscription.is_trial && !subscription.is_daily && ( -
-
-
{t('subscription.autoRenewal')}
-
- {t('subscription.daysBeforeExpiry', { count: subscription.autopay_days_before })}
- + + {/* ─── Locations ─── */} + {subscription.servers && subscription.servers.length > 0 && ( +
+
+ {t('subscription.locationsLabel')} +
+
+ {subscription.servers.map((server) => ( + + {server.country_code && ( + {getFlagEmoji(server.country_code)} + )} + {server.name} + + ))} +
+
+ )} + + {/* ─── Purchased Traffic Packages ─── */} + {subscription.traffic_purchases && subscription.traffic_purchases.length > 0 && ( +
+
+ {t('subscription.purchasedTraffic')} +
+
+ {subscription.traffic_purchases.map((purchase) => ( +
+
+
+
+ +
+ + {purchase.traffic_gb} {t('common.units.gb')} + +
+
+
+ {purchase.days_remaining === 0 + ? t('subscription.expired') + : t('subscription.days', { count: purchase.days_remaining })} +
+
+ {t('subscription.trafficResetAt')}:{' '} + {new Date(purchase.expires_at).toLocaleDateString(undefined, { + day: '2-digit', + month: '2-digit', + year: 'numeric', + })} +
+
+
+
+
+
+
+ {new Date(purchase.created_at).toLocaleDateString()} + {new Date(purchase.expires_at).toLocaleDateString()} +
+
+ ))} +
+
+ )} + + {/* ─── Autopay Toggle ─── */} + {!subscription.is_trial && !subscription.is_daily && ( +
+
+
+ {t('subscription.autoRenewal')} +
+
+ {t('subscription.daysBeforeExpiry', { + count: subscription.autopay_days_before, + })} +
+
+ +
+ )}
- )} -
+ ); + })() ) : ( -
-
+
+
-
{t('subscription.noSubscription')}
+
{t('subscription.noSubscription')}
)} {/* Daily Subscription Pause */} {subscription && subscription.is_daily && !subscription.is_trial && ( -
+
-

+

{t('subscription.pause.title')}

-
+
{subscription.is_daily_paused ? t('subscription.pause.paused') : t('subscription.pause.active')} @@ -1058,15 +1328,20 @@ export default function Subscription() { @@ -1987,40 +2301,50 @@ export default function Subscription() { {devicesLoading ? (
-
+
) : devicesData && devicesData.devices.length > 0 ? ( -
-
+
+
{devicesData.total} /{' '} {t('subscription.devices', { count: devicesData.device_limit })}
{devicesData.devices.map((device) => (
-
+
-
+
{device.device_model || device.platform}
-
{device.platform}
+
{device.platform}
))}
) : ( -
{t('subscription.noDevices')}
+
+ {t('subscription.noDevices')} +
)}
)} {/* Tariffs Section - Combined Purchase/Extend/Switch like MiniApp */} {isTariffsMode && tariffs.length > 0 && ( -
+
-

+

{subscription?.is_daily && !subscription?.is_trial ? t('subscription.switchTariff.title') : subscription && !subscription.is_trial @@ -2071,15 +2407,26 @@ export default function Subscription() { {/* Trial upgrade prompt */} {subscription?.is_trial && ( -
+
-
+
-
+
{t('subscription.trialUpgrade.title')}
-
+
{t('subscription.trialUpgrade.description')}
@@ -2105,15 +2452,26 @@ export default function Subscription() { purchaseOptions && 'subscription_is_expired' in purchaseOptions && purchaseOptions.subscription_is_expired && ( -
+
-
+
-
+
{t('subscription.expiredBanner.title')}
-
+
{t('subscription.expiredBanner.selectTariff')}
@@ -3160,9 +3518,18 @@ export default function Subscription() { {/* Purchase/Extend Section - Classic Mode */} {classicOptions && classicOptions.periods.length > 0 && ( -
+
-

+

{subscription && !subscription.is_trial ? t('subscription.extend') : t('subscription.getSubscription')} diff --git a/src/utils/glassTheme.ts b/src/utils/glassTheme.ts new file mode 100644 index 0000000..2739180 --- /dev/null +++ b/src/utils/glassTheme.ts @@ -0,0 +1,43 @@ +/** + * Theme-aware glass morphism color tokens. + * Provides consistent colors for the glassmorphic card components + * that work on both dark and light backgrounds. + */ +export function getGlassColors(isDark: boolean) { + return { + // Card container + cardBg: isDark + ? 'linear-gradient(145deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)' + : 'linear-gradient(145deg, rgba(255,255,255,0.85) 0%, rgba(255,255,255,0.6) 100%)', + cardBorder: isDark ? 'rgba(255,255,255,0.07)' : 'rgba(0,0,0,0.08)', + + // Inner sections (cards within cards) + innerBg: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.03)', + innerBorder: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)', + + // Hover states + hoverBg: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)', + hoverBorder: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.1)', + + // Text + text: isDark ? '#fff' : '#1a1a2e', + textSecondary: isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.5)', + textMuted: isDark ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.35)', + textFaint: isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)', + textGhost: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)', + + // Progress bar track + trackBg: isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)', + trackBorder: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)', + + // Code blocks + codeBg: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.04)', + codeBorder: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)', + + // Glow effects — reduced in light mode + glowAlpha: isDark ? '15' : '08', + + // Shadows for light mode depth + shadow: isDark ? 'none' : '0 2px 12px rgba(0,0,0,0.06)', + }; +}