mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
feat: add dashboard sub-components for subscription cards and stats grid
SubscriptionCardActive with animated zone dot, traffic bar, connect button. SubscriptionCardExpired with red theme and renewal actions. TrialOfferCard with stats grid and free/paid activation. StatsGrid 2x2 with font-display values and font-mono labels.
This commit is contained in:
213
src/components/dashboard/StatsGrid.tsx
Normal file
213
src/components/dashboard/StatsGrid.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router';
|
||||
import type { Subscription } from '../../types';
|
||||
import { useCurrency } from '../../hooks/useCurrency';
|
||||
|
||||
interface StatsGridProps {
|
||||
balanceRubles: number;
|
||||
subscription: Subscription | null;
|
||||
subLoading: boolean;
|
||||
referralCount: number;
|
||||
earningsRubles: number;
|
||||
refLoading: boolean;
|
||||
}
|
||||
|
||||
const ArrowRightIcon = () => (
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const WalletIcon = () => (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 110-6h5.25A2.25 2.25 0 0121 6v6zm0 0v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18V6a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 6"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CalendarIcon = () => (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const UsersIcon = () => (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CoinIcon = () => (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function StatsGrid({
|
||||
balanceRubles,
|
||||
subscription,
|
||||
subLoading,
|
||||
referralCount,
|
||||
earningsRubles,
|
||||
refLoading,
|
||||
}: StatsGridProps) {
|
||||
const { t } = useTranslation();
|
||||
const { formatAmount, currencySymbol, formatPositive } = useCurrency();
|
||||
|
||||
return (
|
||||
<div className="bento-grid">
|
||||
{/* Balance */}
|
||||
<Link to="/balance" className="bento-card-hover group" data-onboarding="balance">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-accent-500/15 text-accent-400">
|
||||
<WalletIcon />
|
||||
</div>
|
||||
<span className="text-dark-600 transition-colors group-hover:text-accent-400">
|
||||
<ArrowRightIcon />
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-mono text-xs uppercase tracking-wider text-dark-500">
|
||||
{t('dashboard.stats.balance')}
|
||||
</div>
|
||||
<div className="mt-1 font-display text-2xl font-bold text-accent-400">
|
||||
{formatAmount(balanceRubles)}
|
||||
<span className="ml-1 text-base text-dark-500">{currencySymbol}</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Subscription Days */}
|
||||
<Link
|
||||
to="/subscription"
|
||||
className="bento-card-hover group"
|
||||
data-onboarding="subscription-status"
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-accent-500/15 text-accent-400">
|
||||
<CalendarIcon />
|
||||
</div>
|
||||
<span className="text-dark-600 transition-colors group-hover:text-accent-400">
|
||||
<ArrowRightIcon />
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-mono text-xs uppercase tracking-wider text-dark-500">
|
||||
{t('dashboard.stats.subscription')}
|
||||
</div>
|
||||
{subLoading ? (
|
||||
<div className="skeleton mt-1 h-8 w-16" />
|
||||
) : subscription ? (
|
||||
<div className="mt-1 font-display text-2xl font-bold text-dark-50">
|
||||
{subscription.days_left > 0 ? (
|
||||
<>
|
||||
{subscription.days_left}
|
||||
<span className="ml-1 text-base text-dark-500">{t('subscription.days')}</span>
|
||||
</>
|
||||
) : subscription.hours_left > 0 ? (
|
||||
<>
|
||||
{subscription.hours_left}
|
||||
<span className="ml-1 text-base text-dark-500">{t('subscription.hours')}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-error-400">{t('subscription.expired')}</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-1 font-display text-lg font-bold text-error-400">
|
||||
{t('subscription.inactive')}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Referrals */}
|
||||
<Link to="/referral" className="bento-card-hover group">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-accent-500/15 text-accent-400">
|
||||
<UsersIcon />
|
||||
</div>
|
||||
<span className="text-dark-600 transition-colors group-hover:text-accent-400">
|
||||
<ArrowRightIcon />
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-mono text-xs uppercase tracking-wider text-dark-500">
|
||||
{t('dashboard.stats.referrals')}
|
||||
</div>
|
||||
{refLoading ? (
|
||||
<div className="skeleton mt-1 h-8 w-12" />
|
||||
) : (
|
||||
<div className="mt-1 font-display text-2xl font-bold text-dark-50">{referralCount}</div>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Earnings */}
|
||||
<Link to="/referral" className="bento-card-hover group">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-success-500/15 text-success-400">
|
||||
<CoinIcon />
|
||||
</div>
|
||||
<span className="text-dark-600 transition-colors group-hover:text-accent-400">
|
||||
<ArrowRightIcon />
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-mono text-xs uppercase tracking-wider text-dark-500">
|
||||
{t('dashboard.stats.earnings')}
|
||||
</div>
|
||||
{refLoading ? (
|
||||
<div className="skeleton mt-1 h-8 w-16" />
|
||||
) : (
|
||||
<div className="mt-1 font-display text-2xl font-bold text-success-400">
|
||||
{formatPositive(earningsRubles)}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
191
src/components/dashboard/SubscriptionCardActive.tsx
Normal file
191
src/components/dashboard/SubscriptionCardActive.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useNavigate } from 'react-router';
|
||||
import { UseMutationResult } from '@tanstack/react-query';
|
||||
import { HoverBorderGradient } from '../ui/hover-border-gradient';
|
||||
import TrafficProgressBar from './TrafficProgressBar';
|
||||
import { useAnimatedNumber } from '../../hooks/useAnimatedNumber';
|
||||
import { getTrafficZone } from '../../utils/trafficZone';
|
||||
import type { Subscription } from '../../types';
|
||||
|
||||
interface SubscriptionCardActiveProps {
|
||||
subscription: Subscription;
|
||||
trafficData: {
|
||||
traffic_used_gb: number;
|
||||
traffic_used_percent: number;
|
||||
is_unlimited: boolean;
|
||||
} | null;
|
||||
refreshTrafficMutation: UseMutationResult<unknown, unknown, void, unknown>;
|
||||
trafficRefreshCooldown: number;
|
||||
}
|
||||
|
||||
const RefreshIcon = ({ className = 'w-4 h-4' }: { className?: string }) => (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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>
|
||||
);
|
||||
|
||||
const DeviceIcon = () => (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function SubscriptionCardActive({
|
||||
subscription,
|
||||
trafficData,
|
||||
refreshTrafficMutation,
|
||||
trafficRefreshCooldown,
|
||||
}: SubscriptionCardActiveProps) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
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 animatedPercent = useAnimatedNumber(usedPercent);
|
||||
|
||||
const formattedDate = new Date(subscription.end_date).toLocaleDateString();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bento-card ${subscription.is_trial ? 'animate-trial-glow border-warning-500/30 bg-gradient-to-br from-warning-500/5 to-transparent' : ''}`}
|
||||
>
|
||||
{/* Top row: zone indicator + tariff info */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Animated zone dot */}
|
||||
<span className="relative flex h-2.5 w-2.5" aria-hidden="true">
|
||||
<span
|
||||
className={`absolute inline-flex h-full w-full animate-ping rounded-full opacity-75 ${zone.dotClass}`}
|
||||
/>
|
||||
<span className={`relative inline-flex h-2.5 w-2.5 rounded-full ${zone.dotClass}`} />
|
||||
</span>
|
||||
<span className={`text-sm font-medium ${zone.textClass}`}>
|
||||
{isUnlimited ? t('dashboard.unlimited') : t(zone.labelKey)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={
|
||||
subscription.is_trial
|
||||
? 'badge-warning'
|
||||
: subscription.is_active
|
||||
? 'badge-success'
|
||||
: 'badge-error'
|
||||
}
|
||||
>
|
||||
{subscription.is_trial
|
||||
? t('subscription.trialStatus')
|
||||
: subscription.is_active
|
||||
? t('subscription.active')
|
||||
: t('subscription.expired')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Tariff info line */}
|
||||
<div className="mb-4 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-dark-400">
|
||||
{subscription.tariff_name && (
|
||||
<>
|
||||
<span className="text-accent-400">{subscription.tariff_name}</span>
|
||||
<span className="text-dark-600">·</span>
|
||||
</>
|
||||
)}
|
||||
<span>{t('dashboard.validUntil', { date: formattedDate })}</span>
|
||||
<span className="text-dark-600">·</span>
|
||||
<span>
|
||||
{subscription.device_limit} {t('subscription.devices')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Big percentage or infinity */}
|
||||
<div className="mb-1 flex items-end gap-3">
|
||||
{isUnlimited ? (
|
||||
<span className="font-display text-4xl font-bold text-accent-400">∞</span>
|
||||
) : (
|
||||
<span className={`font-display text-4xl font-bold ${zone.textClass}`}>
|
||||
{animatedPercent.toFixed(1)}
|
||||
<span className="text-2xl text-dark-500">%</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Traffic used line with refresh */}
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span className="font-mono text-sm text-dark-400">
|
||||
{isUnlimited
|
||||
? `${usedGb.toFixed(1)} ${t('common.units.gb')}`
|
||||
: `${usedGb.toFixed(1)} / ${subscription.traffic_limit_gb} ${t('common.units.gb')}`}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => refreshTrafficMutation.mutate()}
|
||||
disabled={refreshTrafficMutation.isPending || trafficRefreshCooldown > 0}
|
||||
className="rounded-full p-1 text-dark-500 transition-colors hover:bg-dark-700/50 hover:text-accent-400 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
aria-label={t('common.refresh')}
|
||||
title={trafficRefreshCooldown > 0 ? `${trafficRefreshCooldown}s` : t('common.refresh')}
|
||||
>
|
||||
<RefreshIcon
|
||||
className={`h-3.5 w-3.5 ${refreshTrafficMutation.isPending ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<TrafficProgressBar
|
||||
usedGb={usedGb}
|
||||
limitGb={subscription.traffic_limit_gb}
|
||||
percent={usedPercent}
|
||||
isUnlimited={isUnlimited}
|
||||
showScale={!isUnlimited && subscription.traffic_limit_gb > 0}
|
||||
showThresholds={!isUnlimited}
|
||||
/>
|
||||
|
||||
{/* Connect device button */}
|
||||
{subscription.subscription_url && (
|
||||
<div className="mt-5">
|
||||
<HoverBorderGradient
|
||||
onClick={() => navigate('/connection')}
|
||||
containerClassName="w-full"
|
||||
className="flex w-full items-center justify-center gap-3 py-3"
|
||||
data-onboarding="connect-devices"
|
||||
>
|
||||
<DeviceIcon />
|
||||
<span>{t('dashboard.connectDevice')}</span>
|
||||
</HoverBorderGradient>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom link */}
|
||||
<div className="mt-5 flex items-center justify-end">
|
||||
<Link
|
||||
to="/subscription"
|
||||
className="text-sm font-medium text-accent-400 transition-colors hover:text-accent-300"
|
||||
>
|
||||
{t('dashboard.viewSubscription')} →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
88
src/components/dashboard/SubscriptionCardExpired.tsx
Normal file
88
src/components/dashboard/SubscriptionCardExpired.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router';
|
||||
import type { Subscription } from '../../types';
|
||||
|
||||
interface SubscriptionCardExpiredProps {
|
||||
subscription: Subscription;
|
||||
}
|
||||
|
||||
const ClockIcon = () => (
|
||||
<svg
|
||||
className="h-6 w-6 text-error-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function SubscriptionCardExpired({ subscription }: SubscriptionCardExpiredProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formattedDate = new Date(subscription.end_date).toLocaleDateString();
|
||||
|
||||
return (
|
||||
<div className="bento-card border-error-500/30 bg-gradient-to-br from-error-500/5 to-transparent">
|
||||
{/* Header */}
|
||||
<div className="mb-5 flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-error-500/15">
|
||||
<ClockIcon />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-dark-100">
|
||||
{subscription.is_trial
|
||||
? t('dashboard.expired.trialTitle')
|
||||
: t('dashboard.expired.title')}
|
||||
</h3>
|
||||
</div>
|
||||
<span className="badge-error">{t('subscription.expired')}</span>
|
||||
</div>
|
||||
|
||||
{/* 3-column info */}
|
||||
<div className="mb-5 grid grid-cols-3 gap-4 rounded-xl bg-dark-800/40 p-4">
|
||||
<div className="text-center">
|
||||
<div className="font-display text-2xl font-bold text-dark-300">0</div>
|
||||
<div className="mt-0.5 font-mono text-xs uppercase tracking-wider text-dark-500">
|
||||
{t('dashboard.expired.traffic')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="font-display text-2xl font-bold text-dark-300">0</div>
|
||||
<div className="mt-0.5 font-mono text-xs uppercase tracking-wider text-dark-500">
|
||||
{t('dashboard.expired.devices')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="font-display text-sm font-bold text-dark-300">{formattedDate}</div>
|
||||
<div className="mt-0.5 font-mono text-xs uppercase tracking-wider text-dark-500">
|
||||
{t('dashboard.expired.expiredDate')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Link
|
||||
to="/subscription"
|
||||
state={{ scrollToExtend: true }}
|
||||
className="flex items-center justify-center rounded-xl bg-gradient-to-r from-error-600 to-error-500 py-2.5 text-center text-sm font-medium text-white transition-opacity hover:opacity-90"
|
||||
>
|
||||
{t('dashboard.expired.renew')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/subscription"
|
||||
className="flex items-center justify-center rounded-xl border border-dark-600 py-2.5 text-center text-sm font-medium text-dark-200 transition-colors hover:border-dark-500 hover:bg-dark-800/50"
|
||||
>
|
||||
{t('dashboard.expired.tariffs')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
170
src/components/dashboard/TrialOfferCard.tsx
Normal file
170
src/components/dashboard/TrialOfferCard.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router';
|
||||
import { UseMutationResult } from '@tanstack/react-query';
|
||||
import { HoverBorderGradient } from '../ui/hover-border-gradient';
|
||||
import type { TrialInfo } from '../../types';
|
||||
import { useCurrency } from '../../hooks/useCurrency';
|
||||
|
||||
interface TrialOfferCardProps {
|
||||
trialInfo: TrialInfo;
|
||||
balanceKopeks: number;
|
||||
balanceRubles: number;
|
||||
activateTrialMutation: UseMutationResult<unknown, unknown, void, unknown>;
|
||||
trialError: string | null;
|
||||
}
|
||||
|
||||
const SparklesIcon = () => (
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const BoltIcon = () => (
|
||||
<svg
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function TrialOfferCard({
|
||||
trialInfo,
|
||||
balanceKopeks,
|
||||
balanceRubles,
|
||||
activateTrialMutation,
|
||||
trialError,
|
||||
}: TrialOfferCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const { formatAmount, currencySymbol } = useCurrency();
|
||||
const isFree = !trialInfo.requires_payment;
|
||||
const canAfford = balanceKopeks >= trialInfo.price_kopeks;
|
||||
|
||||
return (
|
||||
<div className="bento-card animate-trial-glow border-accent-500/30 bg-gradient-to-br from-accent-500/5 to-transparent">
|
||||
{/* Icon + Title */}
|
||||
<div className="mb-4 flex flex-col items-center text-center">
|
||||
<div className="mb-3 flex h-14 w-14 items-center justify-center rounded-2xl bg-accent-500/15 text-accent-400">
|
||||
{isFree ? <SparklesIcon /> : <BoltIcon />}
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-dark-100">
|
||||
{isFree ? t('dashboard.trialOffer.freeTitle') : t('dashboard.trialOffer.paidTitle')}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-dark-400">
|
||||
{isFree ? t('dashboard.trialOffer.freeDesc') : t('dashboard.trialOffer.paidDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Price tag for paid trial */}
|
||||
{!isFree && trialInfo.price_rubles > 0 && (
|
||||
<div className="mb-4 flex items-center justify-center gap-2">
|
||||
<span className="font-display text-2xl font-bold text-accent-400">
|
||||
{trialInfo.price_rubles.toFixed(0)} {currencySymbol}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3-column stats */}
|
||||
<div className="mb-5 grid grid-cols-3 gap-4 rounded-xl bg-dark-800/40 p-4">
|
||||
<div className="text-center">
|
||||
<div className="font-display text-2xl font-bold text-accent-400">
|
||||
{trialInfo.duration_days}
|
||||
</div>
|
||||
<div className="mt-0.5 font-mono text-xs uppercase tracking-wider text-dark-500">
|
||||
{t('subscription.trial.days')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="font-display text-2xl font-bold text-accent-400">
|
||||
{trialInfo.traffic_limit_gb || '∞'}
|
||||
</div>
|
||||
<div className="mt-0.5 font-mono text-xs uppercase tracking-wider text-dark-500">
|
||||
{t('common.units.gb')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="font-display text-2xl font-bold text-accent-400">
|
||||
{trialInfo.device_limit}
|
||||
</div>
|
||||
<div className="mt-0.5 font-mono text-xs uppercase tracking-wider text-dark-500">
|
||||
{t('subscription.trial.devices')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Balance info for paid trial */}
|
||||
{!isFree && trialInfo.price_rubles > 0 && (
|
||||
<div className="mb-4 space-y-2 rounded-xl bg-dark-800/50 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-dark-400">{t('balance.currentBalance')}</span>
|
||||
<span
|
||||
className={`font-display text-sm font-semibold ${canAfford ? 'text-success-400' : 'text-warning-400'}`}
|
||||
>
|
||||
{formatAmount(balanceRubles)} {currencySymbol}
|
||||
</span>
|
||||
</div>
|
||||
{!canAfford && (
|
||||
<div className="text-xs text-warning-400">
|
||||
{t('subscription.trial.insufficientBalance')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{trialError && (
|
||||
<div className="mb-4 rounded-xl border border-error-500/30 bg-error-500/10 p-3 text-center text-sm text-error-400">
|
||||
{trialError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CTA Button */}
|
||||
{!isFree && trialInfo.price_kopeks > 0 ? (
|
||||
canAfford ? (
|
||||
<HoverBorderGradient
|
||||
onClick={() => !activateTrialMutation.isPending && activateTrialMutation.mutate()}
|
||||
containerClassName={`w-full ${activateTrialMutation.isPending ? 'opacity-50' : ''}`}
|
||||
className="flex w-full items-center justify-center"
|
||||
aria-disabled={activateTrialMutation.isPending}
|
||||
>
|
||||
{activateTrialMutation.isPending
|
||||
? t('common.loading')
|
||||
: t('subscription.trial.payAndActivate')}
|
||||
</HoverBorderGradient>
|
||||
) : (
|
||||
<Link to="/balance" className="btn-primary block w-full text-center">
|
||||
{t('subscription.trial.topUpToActivate')}
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<HoverBorderGradient
|
||||
onClick={() => !activateTrialMutation.isPending && activateTrialMutation.mutate()}
|
||||
containerClassName={`w-full ${activateTrialMutation.isPending ? 'opacity-50' : ''}`}
|
||||
className="flex w-full items-center justify-center"
|
||||
aria-disabled={activateTrialMutation.isPending}
|
||||
>
|
||||
{activateTrialMutation.isPending ? t('common.loading') : t('subscription.trial.activate')}
|
||||
</HoverBorderGradient>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user