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 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 }) => (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
style={{ opacity: 0.25, flexShrink: 0 }}
|
||||
style={{ flexShrink: 0 }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M6 4l4 4-4 4"
|
||||
stroke="#fff"
|
||||
stroke={color}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -43,6 +45,8 @@ export default function StatsGrid({
|
||||
}: StatsGridProps) {
|
||||
const { t } = useTranslation();
|
||||
const { formatAmount, currencySymbol, formatPositive } = useCurrency();
|
||||
const { isDark } = useTheme();
|
||||
const g = getGlassColors(isDark);
|
||||
|
||||
const zone = useMemo(
|
||||
() => getTrafficZone(subscription?.traffic_used_percent ?? 0),
|
||||
@@ -81,7 +85,7 @@ export default function StatsGrid({
|
||||
label: t('dashboard.stats.subscription'),
|
||||
value: subscription ? `${subscription.days_left}` : '—',
|
||||
valueSuffix: subscription ? ` ${t('subscription.daysShort')}` : '',
|
||||
valueColor: '#fff',
|
||||
valueColor: g.text,
|
||||
to: '/subscription',
|
||||
icon: (color: string) => (
|
||||
<svg
|
||||
@@ -99,15 +103,15 @@ export default function StatsGrid({
|
||||
<circle cx="7" cy="7" r="1" />
|
||||
</svg>
|
||||
),
|
||||
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) => (
|
||||
<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" />
|
||||
</svg>
|
||||
),
|
||||
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)}
|
||||
</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>
|
||||
<ChevronIcon />
|
||||
<ChevronIcon color={g.textFaint} />
|
||||
</div>
|
||||
|
||||
{/* Value */}
|
||||
@@ -196,7 +200,7 @@ export default function StatsGrid({
|
||||
>
|
||||
{card.value}
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -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({
|
||||
<div
|
||||
className="relative overflow-hidden rounded-3xl backdrop-blur-xl"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(145deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)',
|
||||
background: g.cardBg,
|
||||
border: subscription.is_trial
|
||||
? '1px solid rgba(62,219,176,0.15)'
|
||||
: '1px solid rgba(255,255,255,0.07)',
|
||||
: `1px solid ${g.cardBorder}`,
|
||||
padding: '28px 28px 24px',
|
||||
boxShadow: g.shadow,
|
||||
}}
|
||||
>
|
||||
{/* 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({
|
||||
</div>
|
||||
|
||||
{/* 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')}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -162,17 +166,17 @@ export default function SubscriptionCardActive({
|
||||
>
|
||||
∞
|
||||
</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')}
|
||||
</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)}
|
||||
<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 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)}
|
||||
</div>
|
||||
</>
|
||||
@@ -194,9 +198,13 @@ export default function SubscriptionCardActive({
|
||||
{subscription.subscription_url && (
|
||||
<button
|
||||
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"
|
||||
style={{ fontFamily: 'inherit' }}
|
||||
style={{
|
||||
fontFamily: 'inherit',
|
||||
background: g.innerBg,
|
||||
border: `1px solid ${g.innerBorder}`,
|
||||
}}
|
||||
>
|
||||
{/* Monitor icon */}
|
||||
<div
|
||||
@@ -222,10 +230,10 @@ export default function SubscriptionCardActive({
|
||||
|
||||
{/* Text */}
|
||||
<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')}
|
||||
</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', {
|
||||
used: connectedDevices,
|
||||
max: subscription.device_limit,
|
||||
@@ -240,7 +248,7 @@ export default function SubscriptionCardActive({
|
||||
key={i}
|
||||
className="h-[7px] w-[7px] rounded-full transition-all duration-300"
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
@@ -266,27 +274,27 @@ export default function SubscriptionCardActive({
|
||||
>
|
||||
{t('dashboard.tariff')}
|
||||
</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')}
|
||||
</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 })}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Days remaining */}
|
||||
<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={{
|
||||
border:
|
||||
daysLeft <= 3 ? '1px solid rgba(255,184,0,0.2)' : '1px solid rgba(255,255,255,0.04)',
|
||||
background: g.innerBg,
|
||||
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
|
||||
className="flex h-6 w-6 items-center justify-center rounded-[7px] transition-colors duration-300"
|
||||
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
|
||||
@@ -294,7 +302,7 @@ export default function SubscriptionCardActive({
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={daysLeft <= 3 ? '#FFB800' : 'rgba(255,255,255,0.4)'}
|
||||
stroke={daysLeft <= 3 ? '#FFB800' : g.textSecondary}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -309,11 +317,13 @@ export default function SubscriptionCardActive({
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span
|
||||
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}
|
||||
</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>
|
||||
@@ -323,7 +333,7 @@ export default function SubscriptionCardActive({
|
||||
<button
|
||||
onClick={() => refreshTrafficMutation.mutate()}
|
||||
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')}
|
||||
>
|
||||
<RefreshIcon
|
||||
@@ -333,7 +343,7 @@ export default function SubscriptionCardActive({
|
||||
</button>
|
||||
<Link
|
||||
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')} →
|
||||
</Link>
|
||||
@@ -341,12 +351,15 @@ export default function SubscriptionCardActive({
|
||||
|
||||
{/* ─── Sparkline ─── */}
|
||||
{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">
|
||||
<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')}
|
||||
</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)) })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router';
|
||||
import type { Subscription } from '../../types';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
|
||||
interface SubscriptionCardExpiredProps {
|
||||
subscription: Subscription;
|
||||
@@ -8,6 +10,8 @@ interface SubscriptionCardExpiredProps {
|
||||
|
||||
export default function SubscriptionCardExpired({ subscription }: SubscriptionCardExpiredProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isDark } = useTheme();
|
||||
const g = getGlassColors(isDark);
|
||||
|
||||
const formattedDate = new Date(subscription.end_date).toLocaleDateString();
|
||||
|
||||
@@ -15,9 +19,9 @@ export default function SubscriptionCardExpired({ subscription }: SubscriptionCa
|
||||
<div
|
||||
className="relative overflow-hidden rounded-3xl"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(145deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)',
|
||||
background: g.cardBg,
|
||||
border: '1px solid rgba(255,70,70,0.12)',
|
||||
boxShadow: g.shadow,
|
||||
padding: '28px 28px 24px',
|
||||
}}
|
||||
>
|
||||
@@ -72,10 +76,10 @@ export default function SubscriptionCardExpired({ subscription }: SubscriptionCa
|
||||
</svg>
|
||||
</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')}
|
||||
</h2>
|
||||
<span className="text-xs text-white/35">
|
||||
<span className="text-xs text-dark-50/35">
|
||||
{subscription.is_trial
|
||||
? t('dashboard.expired.trialSubtitle')
|
||||
: t('dashboard.expired.paidSubtitle')}
|
||||
@@ -128,10 +132,10 @@ export default function SubscriptionCardExpired({ subscription }: SubscriptionCa
|
||||
{ label: t('dashboard.expired.expiredDate'), value: formattedDate },
|
||||
].map((item, 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}
|
||||
</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>
|
||||
@@ -151,7 +155,11 @@ export default function SubscriptionCardExpired({ subscription }: SubscriptionCa
|
||||
</Link>
|
||||
<Link
|
||||
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')}
|
||||
</Link>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTrafficZone } from '../../utils/trafficZone';
|
||||
import { formatTraffic } from '../../utils/formatTraffic';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
|
||||
interface TrafficProgressBarProps {
|
||||
usedGb: number;
|
||||
@@ -21,6 +23,8 @@ export default function TrafficProgressBar({
|
||||
compact = false,
|
||||
}: TrafficProgressBarProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isDark } = useTheme();
|
||||
const g = getGlassColors(isDark);
|
||||
const zone = useMemo(() => getTrafficZone(percent), [percent]);
|
||||
const clampedPercent = Math.min(percent, 100);
|
||||
const barHeight = compact ? 8 : 14;
|
||||
@@ -42,7 +46,7 @@ export default function TrafficProgressBar({
|
||||
style={{
|
||||
height: barHeight,
|
||||
borderRadius: 10,
|
||||
background: 'rgba(255,255,255,0.06)',
|
||||
background: g.trackBg,
|
||||
border: `1px solid ${zone.mainHex}20`,
|
||||
}}
|
||||
>
|
||||
@@ -90,7 +94,7 @@ export default function TrafficProgressBar({
|
||||
/>
|
||||
{t('dashboard.unlimitedTraffic')}
|
||||
</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) })}
|
||||
</span>
|
||||
</div>
|
||||
@@ -113,8 +117,8 @@ export default function TrafficProgressBar({
|
||||
style={{
|
||||
height: barHeight,
|
||||
borderRadius: 10,
|
||||
background: 'rgba(255,255,255,0.06)',
|
||||
border: '1px solid rgba(255,255,255,0.04)',
|
||||
background: g.trackBg,
|
||||
border: `1px solid ${g.trackBorder}`,
|
||||
}}
|
||||
>
|
||||
{/* Warning zone tint backgrounds */}
|
||||
@@ -171,7 +175,7 @@ export default function TrafficProgressBar({
|
||||
style={{
|
||||
left: `${threshold}%`,
|
||||
width: 1,
|
||||
background: 'rgba(255,255,255,0.08)',
|
||||
background: g.textGhost,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -199,7 +203,7 @@ export default function TrafficProgressBar({
|
||||
{/* Scale labels */}
|
||||
{!compact && limitGb > 0 && (
|
||||
<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"
|
||||
>
|
||||
{[0, 25, 50, 75, 100].map((v) => (
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Link } from 'react-router';
|
||||
import { UseMutationResult } from '@tanstack/react-query';
|
||||
import type { TrialInfo } from '../../types';
|
||||
import { useCurrency } from '../../hooks/useCurrency';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import { getGlassColors } from '../../utils/glassTheme';
|
||||
|
||||
interface TrialOfferCardProps {
|
||||
trialInfo: TrialInfo;
|
||||
@@ -21,6 +23,8 @@ export default function TrialOfferCard({
|
||||
}: TrialOfferCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const { formatAmount, currencySymbol } = useCurrency();
|
||||
const { isDark } = useTheme();
|
||||
const g = getGlassColors(isDark);
|
||||
const isFree = !trialInfo.requires_payment;
|
||||
const canAfford = balanceKopeks >= trialInfo.price_kopeks;
|
||||
|
||||
@@ -28,9 +32,9 @@ export default function TrialOfferCard({
|
||||
<div
|
||||
className="relative overflow-hidden rounded-3xl text-center"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(145deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)',
|
||||
border: '1px solid rgba(255,255,255,0.07)',
|
||||
background: g.cardBg,
|
||||
border: `1px solid ${g.cardBorder}`,
|
||||
boxShadow: g.shadow,
|
||||
padding: '32px 28px 28px',
|
||||
}}
|
||||
>
|
||||
@@ -115,10 +119,10 @@ export default function TrialOfferCard({
|
||||
</div>
|
||||
|
||||
{/* 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')}
|
||||
</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')}
|
||||
</p>
|
||||
|
||||
@@ -154,10 +158,10 @@ export default function TrialOfferCard({
|
||||
{ value: String(trialInfo.device_limit), label: t('subscription.trial.devices') },
|
||||
].map((stat, i) => (
|
||||
<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}
|
||||
</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>
|
||||
@@ -165,11 +169,11 @@ export default function TrialOfferCard({
|
||||
{/* Balance info for paid trial */}
|
||||
{!isFree && trialInfo.price_rubles > 0 && (
|
||||
<div
|
||||
className="mb-4 space-y-2 rounded-xl bg-white/[0.03] p-4 text-left"
|
||||
style={{ border: '1px solid rgba(255,255,255,0.04)' }}
|
||||
className="mb-4 space-y-2 rounded-xl p-4 text-left"
|
||||
style={{ background: g.innerBg, border: `1px solid ${g.innerBorder}` }}
|
||||
>
|
||||
<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
|
||||
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