mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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
This commit is contained in:
@@ -3,7 +3,9 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import type { Subscription } from '../../types';
|
import type { Subscription } from '../../types';
|
||||||
import { useCurrency } from '../../hooks/useCurrency';
|
import { useCurrency } from '../../hooks/useCurrency';
|
||||||
|
import { useTheme } from '../../hooks/useTheme';
|
||||||
import { getTrafficZone } from '../../utils/trafficZone';
|
import { getTrafficZone } from '../../utils/trafficZone';
|
||||||
|
import { getGlassColors } from '../../utils/glassTheme';
|
||||||
|
|
||||||
interface StatsGridProps {
|
interface StatsGridProps {
|
||||||
balanceRubles: number;
|
balanceRubles: number;
|
||||||
@@ -14,18 +16,18 @@ interface StatsGridProps {
|
|||||||
refLoading: boolean;
|
refLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChevronIcon = () => (
|
const ChevronIcon = ({ color }: { color: string }) => (
|
||||||
<svg
|
<svg
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="none"
|
fill="none"
|
||||||
style={{ opacity: 0.25, flexShrink: 0 }}
|
style={{ flexShrink: 0 }}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M6 4l4 4-4 4"
|
d="M6 4l4 4-4 4"
|
||||||
stroke="#fff"
|
stroke={color}
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@@ -43,6 +45,8 @@ export default function StatsGrid({
|
|||||||
}: StatsGridProps) {
|
}: StatsGridProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { formatAmount, currencySymbol, formatPositive } = useCurrency();
|
const { formatAmount, currencySymbol, formatPositive } = useCurrency();
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
const g = getGlassColors(isDark);
|
||||||
|
|
||||||
const zone = useMemo(
|
const zone = useMemo(
|
||||||
() => getTrafficZone(subscription?.traffic_used_percent ?? 0),
|
() => getTrafficZone(subscription?.traffic_used_percent ?? 0),
|
||||||
@@ -81,7 +85,7 @@ export default function StatsGrid({
|
|||||||
label: t('dashboard.stats.subscription'),
|
label: t('dashboard.stats.subscription'),
|
||||||
value: subscription ? `${subscription.days_left}` : '—',
|
value: subscription ? `${subscription.days_left}` : '—',
|
||||||
valueSuffix: subscription ? ` ${t('subscription.daysShort')}` : '',
|
valueSuffix: subscription ? ` ${t('subscription.daysShort')}` : '',
|
||||||
valueColor: '#fff',
|
valueColor: g.text,
|
||||||
to: '/subscription',
|
to: '/subscription',
|
||||||
icon: (color: string) => (
|
icon: (color: string) => (
|
||||||
<svg
|
<svg
|
||||||
@@ -99,15 +103,15 @@ export default function StatsGrid({
|
|||||||
<circle cx="7" cy="7" r="1" />
|
<circle cx="7" cy="7" r="1" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
iconBg: 'rgba(255,255,255,0.06)',
|
iconBg: g.trackBg,
|
||||||
iconColor: 'rgba(255,255,255,0.45)',
|
iconColor: g.textSecondary,
|
||||||
loading: subLoading,
|
loading: subLoading,
|
||||||
onboarding: 'subscription-status',
|
onboarding: 'subscription-status',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('dashboard.stats.referrals'),
|
label: t('dashboard.stats.referrals'),
|
||||||
value: `${referralCount}`,
|
value: `${referralCount}`,
|
||||||
valueColor: '#fff',
|
valueColor: g.text,
|
||||||
to: '/referral',
|
to: '/referral',
|
||||||
icon: (color: string) => (
|
icon: (color: string) => (
|
||||||
<svg
|
<svg
|
||||||
@@ -126,8 +130,8 @@ export default function StatsGrid({
|
|||||||
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" />
|
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
iconBg: 'rgba(255,255,255,0.06)',
|
iconBg: g.trackBg,
|
||||||
iconColor: 'rgba(255,255,255,0.45)',
|
iconColor: g.textSecondary,
|
||||||
loading: refLoading,
|
loading: refLoading,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -165,9 +169,9 @@ export default function StatsGrid({
|
|||||||
to={card.to}
|
to={card.to}
|
||||||
className="group relative overflow-hidden rounded-[18px] transition-all duration-200"
|
className="group relative overflow-hidden rounded-[18px] transition-all duration-200"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background: g.cardBg,
|
||||||
'linear-gradient(145deg, rgba(255,255,255,0.045) 0%, rgba(255,255,255,0.02) 100%)',
|
border: `1px solid ${g.cardBorder}`,
|
||||||
border: '1px solid rgba(255,255,255,0.06)',
|
boxShadow: g.shadow,
|
||||||
padding: '18px 20px 20px',
|
padding: '18px 20px 20px',
|
||||||
}}
|
}}
|
||||||
data-onboarding={card.onboarding}
|
data-onboarding={card.onboarding}
|
||||||
@@ -181,9 +185,9 @@ export default function StatsGrid({
|
|||||||
>
|
>
|
||||||
{card.icon(card.iconColor)}
|
{card.icon(card.iconColor)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[13px] font-medium text-white/45">{card.label}</span>
|
<span className="text-[13px] font-medium text-dark-50/45">{card.label}</span>
|
||||||
</div>
|
</div>
|
||||||
<ChevronIcon />
|
<ChevronIcon color={g.textFaint} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Value */}
|
{/* Value */}
|
||||||
@@ -196,7 +200,7 @@ export default function StatsGrid({
|
|||||||
>
|
>
|
||||||
{card.value}
|
{card.value}
|
||||||
{card.valueSuffix && (
|
{card.valueSuffix && (
|
||||||
<span className="ml-0.5 text-base font-medium text-white/35">
|
<span className="ml-0.5 text-base font-medium text-dark-50/35">
|
||||||
{card.valueSuffix}
|
{card.valueSuffix}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import { UseMutationResult } from '@tanstack/react-query';
|
|||||||
import TrafficProgressBar from './TrafficProgressBar';
|
import TrafficProgressBar from './TrafficProgressBar';
|
||||||
import Sparkline from './Sparkline';
|
import Sparkline from './Sparkline';
|
||||||
import { useAnimatedNumber } from '../../hooks/useAnimatedNumber';
|
import { useAnimatedNumber } from '../../hooks/useAnimatedNumber';
|
||||||
|
import { useTheme } from '../../hooks/useTheme';
|
||||||
import { getTrafficZone } from '../../utils/trafficZone';
|
import { getTrafficZone } from '../../utils/trafficZone';
|
||||||
import { formatTraffic } from '../../utils/formatTraffic';
|
import { formatTraffic } from '../../utils/formatTraffic';
|
||||||
|
import { getGlassColors } from '../../utils/glassTheme';
|
||||||
import type { Subscription } from '../../types';
|
import type { Subscription } from '../../types';
|
||||||
|
|
||||||
interface SubscriptionCardActiveProps {
|
interface SubscriptionCardActiveProps {
|
||||||
@@ -48,6 +50,8 @@ export default function SubscriptionCardActive({
|
|||||||
}: SubscriptionCardActiveProps) {
|
}: SubscriptionCardActiveProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
const g = getGlassColors(isDark);
|
||||||
|
|
||||||
const usedPercent = trafficData?.traffic_used_percent ?? subscription.traffic_used_percent;
|
const usedPercent = trafficData?.traffic_used_percent ?? subscription.traffic_used_percent;
|
||||||
const usedGb = trafficData?.traffic_used_gb ?? subscription.traffic_used_gb;
|
const usedGb = trafficData?.traffic_used_gb ?? subscription.traffic_used_gb;
|
||||||
@@ -65,12 +69,12 @@ export default function SubscriptionCardActive({
|
|||||||
<div
|
<div
|
||||||
className="relative overflow-hidden rounded-3xl backdrop-blur-xl"
|
className="relative overflow-hidden rounded-3xl backdrop-blur-xl"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background: g.cardBg,
|
||||||
'linear-gradient(145deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)',
|
|
||||||
border: subscription.is_trial
|
border: subscription.is_trial
|
||||||
? '1px solid rgba(62,219,176,0.15)'
|
? '1px solid rgba(62,219,176,0.15)'
|
||||||
: '1px solid rgba(255,255,255,0.07)',
|
: `1px solid ${g.cardBorder}`,
|
||||||
padding: '28px 28px 24px',
|
padding: '28px 28px 24px',
|
||||||
|
boxShadow: g.shadow,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Trial shimmer border */}
|
{/* Trial shimmer border */}
|
||||||
@@ -90,7 +94,7 @@ export default function SubscriptionCardActive({
|
|||||||
width: 200,
|
width: 200,
|
||||||
height: 200,
|
height: 200,
|
||||||
borderRadius: '50%',
|
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',
|
transition: 'background 0.8s ease',
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -147,7 +151,7 @@ export default function SubscriptionCardActive({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h2 className="text-lg font-bold tracking-tight text-white">
|
<h2 className="text-lg font-bold tracking-tight text-dark-50">
|
||||||
{t('dashboard.trafficUsageTitle')}
|
{t('dashboard.trafficUsageTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,17 +166,17 @@ export default function SubscriptionCardActive({
|
|||||||
>
|
>
|
||||||
∞
|
∞
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 font-mono text-[11px] text-white/30">
|
<div className="mt-1 font-mono text-[11px] text-dark-50/30">
|
||||||
{formatTraffic(usedGb)} {t('dashboard.usedSuffix')}
|
{formatTraffic(usedGb)} {t('dashboard.usedSuffix')}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="font-display text-[38px] font-extrabold leading-none tracking-tight text-white">
|
<div className="font-display text-[38px] font-extrabold leading-none tracking-tight text-dark-50">
|
||||||
{animatedPercent.toFixed(0)}
|
{animatedPercent.toFixed(0)}
|
||||||
<span className="ml-px text-lg font-medium text-white/35">%</span>
|
<span className="ml-px text-lg font-medium text-dark-50/35">%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 font-mono text-[11px] text-white/30">
|
<div className="mt-0.5 font-mono text-[11px] text-dark-50/30">
|
||||||
{formatTraffic(usedGb)} / {formatTraffic(subscription.traffic_limit_gb)}
|
{formatTraffic(usedGb)} / {formatTraffic(subscription.traffic_limit_gb)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -194,9 +198,13 @@ export default function SubscriptionCardActive({
|
|||||||
{subscription.subscription_url && (
|
{subscription.subscription_url && (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/connection')}
|
onClick={() => navigate('/connection')}
|
||||||
className="mb-2.5 flex w-full items-center gap-3.5 rounded-[14px] border border-white/[0.04] bg-white/[0.03] p-3.5 text-left transition-all duration-300 hover:border-white/[0.08] hover:bg-white/[0.05]"
|
className="mb-2.5 flex w-full items-center gap-3.5 rounded-[14px] p-3.5 text-left transition-all duration-300"
|
||||||
data-onboarding="connect-devices"
|
data-onboarding="connect-devices"
|
||||||
style={{ fontFamily: 'inherit' }}
|
style={{
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
background: g.innerBg,
|
||||||
|
border: `1px solid ${g.innerBorder}`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Monitor icon */}
|
{/* Monitor icon */}
|
||||||
<div
|
<div
|
||||||
@@ -222,10 +230,10 @@ export default function SubscriptionCardActive({
|
|||||||
|
|
||||||
{/* Text */}
|
{/* Text */}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-sm font-semibold tracking-tight text-white">
|
<div className="text-sm font-semibold tracking-tight text-dark-50">
|
||||||
{t('dashboard.connectDevice')}
|
{t('dashboard.connectDevice')}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 text-[11px] text-white/30">
|
<div className="mt-0.5 text-[11px] text-dark-50/30">
|
||||||
{t('dashboard.devicesOfMax', {
|
{t('dashboard.devicesOfMax', {
|
||||||
used: connectedDevices,
|
used: connectedDevices,
|
||||||
max: subscription.device_limit,
|
max: subscription.device_limit,
|
||||||
@@ -240,7 +248,7 @@ export default function SubscriptionCardActive({
|
|||||||
key={i}
|
key={i}
|
||||||
className="h-[7px] w-[7px] rounded-full transition-all duration-300"
|
className="h-[7px] w-[7px] rounded-full transition-all duration-300"
|
||||||
style={{
|
style={{
|
||||||
background: i < connectedDevices ? zone.mainHex : 'rgba(255,255,255,0.08)',
|
background: i < connectedDevices ? zone.mainHex : g.textGhost,
|
||||||
boxShadow: i < connectedDevices ? `0 0 6px ${zone.mainHex}50` : 'none',
|
boxShadow: i < connectedDevices ? `0 0 6px ${zone.mainHex}50` : 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -266,27 +274,27 @@ export default function SubscriptionCardActive({
|
|||||||
>
|
>
|
||||||
{t('dashboard.tariff')}
|
{t('dashboard.tariff')}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base font-bold leading-tight tracking-tight text-white">
|
<div className="text-base font-bold leading-tight tracking-tight text-dark-50">
|
||||||
{subscription.tariff_name || t('subscription.currentPlan')}
|
{subscription.tariff_name || t('subscription.currentPlan')}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 font-mono text-[10px] text-white/30">
|
<div className="mt-0.5 font-mono text-[10px] text-dark-50/30">
|
||||||
{t('dashboard.validUntil', { date: formattedDate })}
|
{t('dashboard.validUntil', { date: formattedDate })}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Days remaining */}
|
{/* Days remaining */}
|
||||||
<div
|
<div
|
||||||
className="flex-1 rounded-[14px] bg-white/[0.03] p-3.5 transition-colors duration-300"
|
className="flex-1 rounded-[14px] p-3.5 transition-colors duration-300"
|
||||||
style={{
|
style={{
|
||||||
border:
|
background: g.innerBg,
|
||||||
daysLeft <= 3 ? '1px solid rgba(255,184,0,0.2)' : '1px solid rgba(255,255,255,0.04)',
|
border: daysLeft <= 3 ? '1px solid rgba(255,184,0,0.2)' : `1px solid ${g.innerBorder}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mb-1 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-white/35">
|
<div className="mb-1 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-dark-50/35">
|
||||||
<div
|
<div
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-[7px] transition-colors duration-300"
|
className="flex h-6 w-6 items-center justify-center rounded-[7px] transition-colors duration-300"
|
||||||
style={{
|
style={{
|
||||||
background: daysLeft <= 3 ? 'rgba(255,184,0,0.1)' : 'rgba(255,255,255,0.05)',
|
background: daysLeft <= 3 ? 'rgba(255,184,0,0.1)' : g.hoverBg,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -294,7 +302,7 @@ export default function SubscriptionCardActive({
|
|||||||
height="13"
|
height="13"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={daysLeft <= 3 ? '#FFB800' : 'rgba(255,255,255,0.4)'}
|
stroke={daysLeft <= 3 ? '#FFB800' : g.textSecondary}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@@ -309,11 +317,13 @@ export default function SubscriptionCardActive({
|
|||||||
<div className="flex items-baseline gap-1">
|
<div className="flex items-baseline gap-1">
|
||||||
<span
|
<span
|
||||||
className="text-[22px] font-bold tracking-tight transition-colors duration-300"
|
className="text-[22px] font-bold tracking-tight transition-colors duration-300"
|
||||||
style={{ color: daysLeft <= 3 ? '#FFB800' : '#fff' }}
|
style={{ color: daysLeft <= 3 ? '#FFB800' : g.text }}
|
||||||
>
|
>
|
||||||
{daysLeft}
|
{daysLeft}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs font-medium text-white/25">{t('subscription.daysShort')}</span>
|
<span className="text-xs font-medium text-dark-50/25">
|
||||||
|
{t('subscription.daysShort')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -323,7 +333,7 @@ export default function SubscriptionCardActive({
|
|||||||
<button
|
<button
|
||||||
onClick={() => refreshTrafficMutation.mutate()}
|
onClick={() => refreshTrafficMutation.mutate()}
|
||||||
disabled={refreshTrafficMutation.isPending || trafficRefreshCooldown > 0}
|
disabled={refreshTrafficMutation.isPending || trafficRefreshCooldown > 0}
|
||||||
className="flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium text-white/35 transition-colors hover:bg-white/[0.05] hover:text-white/50 disabled:cursor-not-allowed disabled:opacity-50"
|
className="flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium text-dark-50/35 transition-colors hover:bg-dark-50/[0.05] hover:text-dark-50/50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
aria-label={t('common.refresh')}
|
aria-label={t('common.refresh')}
|
||||||
>
|
>
|
||||||
<RefreshIcon
|
<RefreshIcon
|
||||||
@@ -333,7 +343,7 @@ export default function SubscriptionCardActive({
|
|||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
to="/subscription"
|
to="/subscription"
|
||||||
className="text-[11px] font-medium text-white/25 transition-colors hover:text-white/40"
|
className="text-[11px] font-medium text-dark-50/25 transition-colors hover:text-dark-50/40"
|
||||||
>
|
>
|
||||||
{t('dashboard.viewSubscription')} →
|
{t('dashboard.viewSubscription')} →
|
||||||
</Link>
|
</Link>
|
||||||
@@ -341,12 +351,15 @@ export default function SubscriptionCardActive({
|
|||||||
|
|
||||||
{/* ─── Sparkline ─── */}
|
{/* ─── Sparkline ─── */}
|
||||||
{dailyUsage.length >= 2 && (
|
{dailyUsage.length >= 2 && (
|
||||||
<div className="rounded-[14px] border border-white/[0.04] bg-white/[0.02] p-3.5 pb-3">
|
<div
|
||||||
|
className="rounded-[14px] p-3.5 pb-3"
|
||||||
|
style={{ background: g.innerBg, border: `1px solid ${g.innerBorder}` }}
|
||||||
|
>
|
||||||
<div className="mb-2.5 flex items-center justify-between">
|
<div className="mb-2.5 flex items-center justify-between">
|
||||||
<span className="text-[11px] font-medium uppercase tracking-wider text-white/40">
|
<span className="text-[11px] font-medium uppercase tracking-wider text-dark-50/40">
|
||||||
{t('dashboard.usageLast14Days')}
|
{t('dashboard.usageLast14Days')}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-mono text-[11px] text-white/25">
|
<span className="font-mono text-[11px] text-dark-50/25">
|
||||||
{t('dashboard.maxUsage', { amount: formatTraffic(Math.max(...dailyUsage)) })}
|
{t('dashboard.maxUsage', { amount: formatTraffic(Math.max(...dailyUsage)) })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import type { Subscription } from '../../types';
|
import type { Subscription } from '../../types';
|
||||||
|
import { useTheme } from '../../hooks/useTheme';
|
||||||
|
import { getGlassColors } from '../../utils/glassTheme';
|
||||||
|
|
||||||
interface SubscriptionCardExpiredProps {
|
interface SubscriptionCardExpiredProps {
|
||||||
subscription: Subscription;
|
subscription: Subscription;
|
||||||
@@ -8,6 +10,8 @@ interface SubscriptionCardExpiredProps {
|
|||||||
|
|
||||||
export default function SubscriptionCardExpired({ subscription }: SubscriptionCardExpiredProps) {
|
export default function SubscriptionCardExpired({ subscription }: SubscriptionCardExpiredProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
const g = getGlassColors(isDark);
|
||||||
|
|
||||||
const formattedDate = new Date(subscription.end_date).toLocaleDateString();
|
const formattedDate = new Date(subscription.end_date).toLocaleDateString();
|
||||||
|
|
||||||
@@ -15,9 +19,9 @@ export default function SubscriptionCardExpired({ subscription }: SubscriptionCa
|
|||||||
<div
|
<div
|
||||||
className="relative overflow-hidden rounded-3xl"
|
className="relative overflow-hidden rounded-3xl"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background: g.cardBg,
|
||||||
'linear-gradient(145deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)',
|
|
||||||
border: '1px solid rgba(255,70,70,0.12)',
|
border: '1px solid rgba(255,70,70,0.12)',
|
||||||
|
boxShadow: g.shadow,
|
||||||
padding: '28px 28px 24px',
|
padding: '28px 28px 24px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -72,10 +76,10 @@ export default function SubscriptionCardExpired({ subscription }: SubscriptionCa
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-bold tracking-tight text-white">
|
<h2 className="text-lg font-bold tracking-tight text-dark-50">
|
||||||
{t('dashboard.expired.title')}
|
{t('dashboard.expired.title')}
|
||||||
</h2>
|
</h2>
|
||||||
<span className="text-xs text-white/35">
|
<span className="text-xs text-dark-50/35">
|
||||||
{subscription.is_trial
|
{subscription.is_trial
|
||||||
? t('dashboard.expired.trialSubtitle')
|
? t('dashboard.expired.trialSubtitle')
|
||||||
: t('dashboard.expired.paidSubtitle')}
|
: t('dashboard.expired.paidSubtitle')}
|
||||||
@@ -128,10 +132,10 @@ export default function SubscriptionCardExpired({ subscription }: SubscriptionCa
|
|||||||
{ label: t('dashboard.expired.expiredDate'), value: formattedDate },
|
{ label: t('dashboard.expired.expiredDate'), value: formattedDate },
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
<div className="mb-1 font-mono text-[10px] font-medium uppercase tracking-wider text-white/30">
|
<div className="mb-1 font-mono text-[10px] font-medium uppercase tracking-wider text-dark-50/30">
|
||||||
{item.label}
|
{item.label}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base font-bold tracking-tight text-white/50">{item.value}</div>
|
<div className="text-base font-bold tracking-tight text-dark-50/50">{item.value}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -151,7 +155,11 @@ export default function SubscriptionCardExpired({ subscription }: SubscriptionCa
|
|||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/subscription"
|
to="/subscription"
|
||||||
className="flex items-center justify-center rounded-[14px] border border-white/[0.08] bg-white/[0.03] px-5 py-3.5 text-[15px] font-semibold tracking-tight text-white/50 transition-colors duration-200 hover:bg-white/[0.05]"
|
className="flex items-center justify-center rounded-[14px] px-5 py-3.5 text-[15px] font-semibold tracking-tight text-dark-50/50 transition-colors duration-200"
|
||||||
|
style={{
|
||||||
|
background: g.innerBg,
|
||||||
|
border: `1px solid ${g.innerBorder}`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t('dashboard.expired.tariffs')}
|
{t('dashboard.expired.tariffs')}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useMemo } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { getTrafficZone } from '../../utils/trafficZone';
|
import { getTrafficZone } from '../../utils/trafficZone';
|
||||||
import { formatTraffic } from '../../utils/formatTraffic';
|
import { formatTraffic } from '../../utils/formatTraffic';
|
||||||
|
import { useTheme } from '../../hooks/useTheme';
|
||||||
|
import { getGlassColors } from '../../utils/glassTheme';
|
||||||
|
|
||||||
interface TrafficProgressBarProps {
|
interface TrafficProgressBarProps {
|
||||||
usedGb: number;
|
usedGb: number;
|
||||||
@@ -21,6 +23,8 @@ export default function TrafficProgressBar({
|
|||||||
compact = false,
|
compact = false,
|
||||||
}: TrafficProgressBarProps) {
|
}: TrafficProgressBarProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
const g = getGlassColors(isDark);
|
||||||
const zone = useMemo(() => getTrafficZone(percent), [percent]);
|
const zone = useMemo(() => getTrafficZone(percent), [percent]);
|
||||||
const clampedPercent = Math.min(percent, 100);
|
const clampedPercent = Math.min(percent, 100);
|
||||||
const barHeight = compact ? 8 : 14;
|
const barHeight = compact ? 8 : 14;
|
||||||
@@ -42,7 +46,7 @@ export default function TrafficProgressBar({
|
|||||||
style={{
|
style={{
|
||||||
height: barHeight,
|
height: barHeight,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
background: 'rgba(255,255,255,0.06)',
|
background: g.trackBg,
|
||||||
border: `1px solid ${zone.mainHex}20`,
|
border: `1px solid ${zone.mainHex}20`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -90,7 +94,7 @@ export default function TrafficProgressBar({
|
|||||||
/>
|
/>
|
||||||
{t('dashboard.unlimitedTraffic')}
|
{t('dashboard.unlimitedTraffic')}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-mono text-[11px] text-white/30">
|
<span className="font-mono text-[11px] text-dark-50/30">
|
||||||
{t('dashboard.usedTraffic', { amount: formatTraffic(usedGb) })}
|
{t('dashboard.usedTraffic', { amount: formatTraffic(usedGb) })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,8 +117,8 @@ export default function TrafficProgressBar({
|
|||||||
style={{
|
style={{
|
||||||
height: barHeight,
|
height: barHeight,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
background: 'rgba(255,255,255,0.06)',
|
background: g.trackBg,
|
||||||
border: '1px solid rgba(255,255,255,0.04)',
|
border: `1px solid ${g.trackBorder}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Warning zone tint backgrounds */}
|
{/* Warning zone tint backgrounds */}
|
||||||
@@ -171,7 +175,7 @@ export default function TrafficProgressBar({
|
|||||||
style={{
|
style={{
|
||||||
left: `${threshold}%`,
|
left: `${threshold}%`,
|
||||||
width: 1,
|
width: 1,
|
||||||
background: 'rgba(255,255,255,0.08)',
|
background: g.textGhost,
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
@@ -199,7 +203,7 @@ export default function TrafficProgressBar({
|
|||||||
{/* Scale labels */}
|
{/* Scale labels */}
|
||||||
{!compact && limitGb > 0 && (
|
{!compact && limitGb > 0 && (
|
||||||
<div
|
<div
|
||||||
className="mt-1.5 flex justify-between px-0.5 font-mono text-[9px] font-medium text-white/20"
|
className="mt-1.5 flex justify-between px-0.5 font-mono text-[9px] font-medium text-dark-50/20"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{[0, 25, 50, 75, 100].map((v) => (
|
{[0, 25, 50, 75, 100].map((v) => (
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { Link } from 'react-router';
|
|||||||
import { UseMutationResult } from '@tanstack/react-query';
|
import { UseMutationResult } from '@tanstack/react-query';
|
||||||
import type { TrialInfo } from '../../types';
|
import type { TrialInfo } from '../../types';
|
||||||
import { useCurrency } from '../../hooks/useCurrency';
|
import { useCurrency } from '../../hooks/useCurrency';
|
||||||
|
import { useTheme } from '../../hooks/useTheme';
|
||||||
|
import { getGlassColors } from '../../utils/glassTheme';
|
||||||
|
|
||||||
interface TrialOfferCardProps {
|
interface TrialOfferCardProps {
|
||||||
trialInfo: TrialInfo;
|
trialInfo: TrialInfo;
|
||||||
@@ -21,6 +23,8 @@ export default function TrialOfferCard({
|
|||||||
}: TrialOfferCardProps) {
|
}: TrialOfferCardProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { formatAmount, currencySymbol } = useCurrency();
|
const { formatAmount, currencySymbol } = useCurrency();
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
const g = getGlassColors(isDark);
|
||||||
const isFree = !trialInfo.requires_payment;
|
const isFree = !trialInfo.requires_payment;
|
||||||
const canAfford = balanceKopeks >= trialInfo.price_kopeks;
|
const canAfford = balanceKopeks >= trialInfo.price_kopeks;
|
||||||
|
|
||||||
@@ -28,9 +32,9 @@ export default function TrialOfferCard({
|
|||||||
<div
|
<div
|
||||||
className="relative overflow-hidden rounded-3xl text-center"
|
className="relative overflow-hidden rounded-3xl text-center"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background: g.cardBg,
|
||||||
'linear-gradient(145deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)',
|
border: `1px solid ${g.cardBorder}`,
|
||||||
border: '1px solid rgba(255,255,255,0.07)',
|
boxShadow: g.shadow,
|
||||||
padding: '32px 28px 28px',
|
padding: '32px 28px 28px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -115,10 +119,10 @@ export default function TrialOfferCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h2 className="mb-1.5 text-[22px] font-bold tracking-tight text-white">
|
<h2 className="mb-1.5 text-[22px] font-bold tracking-tight text-dark-50">
|
||||||
{isFree ? t('dashboard.trialOffer.freeTitle') : t('dashboard.trialOffer.paidTitle')}
|
{isFree ? t('dashboard.trialOffer.freeTitle') : t('dashboard.trialOffer.paidTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mb-5 text-sm text-white/40">
|
<p className="mb-5 text-sm text-dark-50/40">
|
||||||
{isFree ? t('dashboard.trialOffer.freeDesc') : t('dashboard.trialOffer.paidDesc')}
|
{isFree ? t('dashboard.trialOffer.freeDesc') : t('dashboard.trialOffer.paidDesc')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -154,10 +158,10 @@ export default function TrialOfferCard({
|
|||||||
{ value: String(trialInfo.device_limit), label: t('subscription.trial.devices') },
|
{ value: String(trialInfo.device_limit), label: t('subscription.trial.devices') },
|
||||||
].map((stat, i) => (
|
].map((stat, i) => (
|
||||||
<div key={i} className="text-center">
|
<div key={i} className="text-center">
|
||||||
<div className="text-4xl font-extrabold leading-none tracking-tight text-white">
|
<div className="text-4xl font-extrabold leading-none tracking-tight text-dark-50">
|
||||||
{stat.value}
|
{stat.value}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs font-medium text-white/30">{stat.label}</div>
|
<div className="mt-1 text-xs font-medium text-dark-50/30">{stat.label}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -165,11 +169,11 @@ export default function TrialOfferCard({
|
|||||||
{/* Balance info for paid trial */}
|
{/* Balance info for paid trial */}
|
||||||
{!isFree && trialInfo.price_rubles > 0 && (
|
{!isFree && trialInfo.price_rubles > 0 && (
|
||||||
<div
|
<div
|
||||||
className="mb-4 space-y-2 rounded-xl bg-white/[0.03] p-4 text-left"
|
className="mb-4 space-y-2 rounded-xl p-4 text-left"
|
||||||
style={{ border: '1px solid rgba(255,255,255,0.04)' }}
|
style={{ background: g.innerBg, border: `1px solid ${g.innerBorder}` }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm text-white/40">{t('balance.currentBalance')}</span>
|
<span className="text-sm text-dark-50/40">{t('balance.currentBalance')}</span>
|
||||||
<span
|
<span
|
||||||
className={`font-display text-sm font-semibold ${canAfford ? 'text-success-400' : 'text-warning-400'}`}
|
className={`font-display text-sm font-semibold ${canAfford ? 'text-success-400' : 'text-warning-400'}`}
|
||||||
>
|
>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
43
src/utils/glassTheme.ts
Normal file
43
src/utils/glassTheme.ts
Normal file
@@ -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)',
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user