Merge pull request #406 from BEDOLAGA-DEV/dev

Dev
This commit is contained in:
Egor
2026-04-23 05:20:09 +03:00
committed by GitHub
13 changed files with 186 additions and 169 deletions

View File

@@ -228,7 +228,7 @@ export default function SubscriptionCardExpired({
> >
<div className="flex items-center"> <div className="flex items-center">
<div className="mb-0.5 font-mono text-[10px] font-medium uppercase tracking-wider text-dark-50/30"> <div className="mb-0.5 font-mono text-[10px] font-medium uppercase tracking-wider text-dark-50/30">
{t('dashboard.expired.expiredDate')} {isLimited ? t('dashboard.expired.activeUntil') : t('dashboard.expired.expiredDate')}
</div> </div>
<div className="ml-3 text-base font-bold tracking-tight text-dark-50/50"> <div className="ml-3 text-base font-bold tracking-tight text-dark-50/50">
{formattedDate} {formattedDate}
@@ -280,7 +280,7 @@ export default function SubscriptionCardExpired({
> >
<path d="M12 4.5v15m7.5-7.5h-15" /> <path d="M12 4.5v15m7.5-7.5h-15" />
</svg> </svg>
{t('subscription.trafficLimited')} {t('subscription.buyTraffic')}
</Link> </Link>
) : ( ) : (
<> <>

View File

@@ -15,7 +15,9 @@ export default function PurchaseCTAButton({
}: PurchaseCTAButtonProps) { }: PurchaseCTAButtonProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const isExpired = !subscription || (!subscription.is_active && !subscription.is_trial); const isExpired =
!subscription ||
(!subscription.is_active && !subscription.is_trial && !subscription.is_limited);
const isTrial = subscription?.is_trial; const isTrial = subscription?.is_trial;
const isDaily = subscription?.is_daily; const isDaily = subscription?.is_daily;

View File

@@ -82,7 +82,10 @@ export default function SubscriptionListCard({
}; };
const isTrial = subscription.is_trial; const isTrial = subscription.is_trial;
const isActive = subscription.status === 'active' || subscription.status === 'trial'; const isActive =
subscription.status === 'active' ||
subscription.status === 'trial' ||
subscription.status === 'limited';
const isExpired = subscription.status === 'expired' || subscription.status === 'disabled'; const isExpired = subscription.status === 'expired' || subscription.status === 'disabled';
const trafficLimit = subscription.traffic_limit_gb; const trafficLimit = subscription.traffic_limit_gb;
const trafficUsed = subscription.traffic_used_gb; const trafficUsed = subscription.traffic_used_gb;
@@ -95,21 +98,25 @@ export default function SubscriptionListCard({
const trafficColor = const trafficColor =
trafficPercent >= 90 ? 'bg-red-400' : trafficPercent >= 70 ? 'bg-amber-400' : 'bg-emerald-400'; trafficPercent >= 90 ? 'bg-red-400' : trafficPercent >= 70 ? 'bg-amber-400' : 'bg-emerald-400';
const borderColor = isTrial const isLimitedStatus = subscription.status === 'limited';
? 'rgba(251,191,36,0.2)'
: isExpired
? 'rgba(255,59,92,0.15)'
: g.cardBorder;
const bgColor = isTrial const borderColor =
? isDark isTrial || isLimitedStatus
? 'rgba(251,191,36,0.04)' ? 'rgba(251,191,36,0.2)'
: 'rgba(251,191,36,0.03)' : isExpired
: isExpired ? 'rgba(255,59,92,0.15)'
: g.cardBorder;
const bgColor =
isTrial || isLimitedStatus
? isDark ? isDark
? 'rgba(255,59,92,0.04)' ? 'rgba(251,191,36,0.04)'
: 'rgba(255,59,92,0.03)' : 'rgba(251,191,36,0.03)'
: g.cardBg; : isExpired
? isDark
? 'rgba(255,59,92,0.04)'
: 'rgba(255,59,92,0.03)'
: g.cardBg;
return ( return (
<button <button

View File

@@ -272,7 +272,8 @@
"tariffs": "Tariffs", "tariffs": "Tariffs",
"traffic": "Traffic", "traffic": "Traffic",
"devices": "Devices", "devices": "Devices",
"expiredDate": "Expired" "expiredDate": "Expired",
"activeUntil": "Active until"
}, },
"suspended": { "suspended": {
"title": "Subscription Suspended", "title": "Subscription Suspended",
@@ -554,8 +555,8 @@
"unlimited": "Unlimited", "unlimited": "Unlimited",
"buyTrafficGb": "Buy {{gb}} GB", "buyTrafficGb": "Buy {{gb}} GB",
"buyUnlimited": "Buy Unlimited", "buyUnlimited": "Buy Unlimited",
"manageServers": "Server management", "manageServers": "Squad management",
"manageServersTitle": "Server management" "manageServersTitle": "Squad management"
}, },
"serverManagement": { "serverManagement": {
"statusLegend": "✅ — connected • — will be added (paid) • — will be disconnected", "statusLegend": "✅ — connected • — will be added (paid) • — will be disconnected",
@@ -1099,7 +1100,7 @@
"apps": "Apps", "apps": "Apps",
"wheel": "Wheel", "wheel": "Wheel",
"tariffs": "Tariffs", "tariffs": "Tariffs",
"servers": "Servers", "servers": "Squads",
"banSystem": "Ban Monitoring", "banSystem": "Ban Monitoring",
"broadcasts": "Broadcasts", "broadcasts": "Broadcasts",
"users": "Users", "users": "Users",
@@ -2350,8 +2351,8 @@
} }
}, },
"servers": { "servers": {
"title": "Server Management", "title": "Squad Management",
"subtitle": "Configure VPN servers", "subtitle": "Configure squads",
"sync": "Sync", "sync": "Sync",
"syncing": "Syncing...", "syncing": "Syncing...",
"syncNow": "Sync now", "syncNow": "Sync now",

View File

@@ -386,8 +386,8 @@
"unlimited": "نامحدود", "unlimited": "نامحدود",
"buyTrafficGb": "خرید {{gb}} GB", "buyTrafficGb": "خرید {{gb}} GB",
"buyUnlimited": "خرید نامحدود", "buyUnlimited": "خرید نامحدود",
"manageServers": "مدیریت سرورها", "manageServers": "مدیریت اسکوادها",
"manageServersTitle": "مدیریت سرورها" "manageServersTitle": "مدیریت اسکوادها"
}, },
"serverManagement": { "serverManagement": {
"statusLegend": "✅ — متصل • — اضافه خواهد شد (پولی) • — قطع خواهد شد", "statusLegend": "✅ — متصل • — اضافه خواهد شد (پولی) • — قطع خواهد شد",
@@ -924,7 +924,7 @@
"apps": "برنامه‌ها", "apps": "برنامه‌ها",
"wheel": "چرخ", "wheel": "چرخ",
"tariffs": "تعرفه‌ها", "tariffs": "تعرفه‌ها",
"servers": "سرورها", "servers": "اسکوادها",
"broadcasts": "پیام‌رسانی", "broadcasts": "پیام‌رسانی",
"emailTemplates": "قالب‌های ایمیل", "emailTemplates": "قالب‌های ایمیل",
"paymentMethods": "روش‌های پرداخت", "paymentMethods": "روش‌های پرداخت",
@@ -1986,8 +1986,8 @@
} }
}, },
"servers": { "servers": {
"title": "مدیریت سرورها", "title": "مدیریت اسکوادها",
"subtitle": "پیکربندی سرورهای VPN", "subtitle": "پیکربندی اسکوادها",
"sync": "همگام‌سازی", "sync": "همگام‌سازی",
"syncing": "در حال همگام‌سازی...", "syncing": "در حال همگام‌سازی...",
"syncNow": "همگام‌سازی الان", "syncNow": "همگام‌سازی الان",

View File

@@ -284,7 +284,8 @@
"tariffs": "Тарифы", "tariffs": "Тарифы",
"traffic": "Трафик", "traffic": "Трафик",
"devices": "Устройства", "devices": "Устройства",
"expiredDate": "Истекла" "expiredDate": "Истекла",
"activeUntil": "Активна до"
}, },
"suspended": { "suspended": {
"title": "Подписка приостановлена", "title": "Подписка приостановлена",
@@ -582,8 +583,8 @@
"unlimited": "Безлимит", "unlimited": "Безлимит",
"buyTrafficGb": "Купить {{gb}} ГБ", "buyTrafficGb": "Купить {{gb}} ГБ",
"buyUnlimited": "Купить Безлимит", "buyUnlimited": "Купить Безлимит",
"manageServers": "Управление серверами", "manageServers": "Управление сквадами",
"manageServersTitle": "Управление серверами" "manageServersTitle": "Управление сквадами"
}, },
"serverManagement": { "serverManagement": {
"statusLegend": "✅ — подключено • — будет добавлено (платно) • — будет отключено", "statusLegend": "✅ — подключено • — будет добавлено (платно) • — будет отключено",
@@ -1120,7 +1121,7 @@
"apps": "Приложения", "apps": "Приложения",
"wheel": "Колесо", "wheel": "Колесо",
"tariffs": "Тарифы", "tariffs": "Тарифы",
"servers": "Серверы", "servers": "Сквады",
"banSystem": "Мониторинг банов", "banSystem": "Мониторинг банов",
"broadcasts": "Рассылки", "broadcasts": "Рассылки",
"users": "Пользователи", "users": "Пользователи",
@@ -2856,8 +2857,8 @@
} }
}, },
"servers": { "servers": {
"title": "Управление серверами", "title": "Управление сквадами",
"subtitle": "Настройка серверов VPN", "subtitle": "Настройка сквадов",
"sync": "Синхронизировать", "sync": "Синхронизировать",
"syncing": "Синхронизация...", "syncing": "Синхронизация...",
"syncNow": "Синхронизировать сейчас", "syncNow": "Синхронизировать сейчас",

View File

@@ -386,8 +386,8 @@
"unlimited": "无限制", "unlimited": "无限制",
"buyTrafficGb": "购买 {{gb}} GB", "buyTrafficGb": "购买 {{gb}} GB",
"buyUnlimited": "购买无限流量", "buyUnlimited": "购买无限流量",
"manageServers": "服务器管理", "manageServers": "小队管理",
"manageServersTitle": "服务器管理" "manageServersTitle": "小队管理"
}, },
"serverManagement": { "serverManagement": {
"statusLegend": "✅ — 已连接 • — 将添加(付费)• — 将断开", "statusLegend": "✅ — 已连接 • — 将添加(付费)• — 将断开",
@@ -924,7 +924,7 @@
"apps": "应用", "apps": "应用",
"wheel": "转盘", "wheel": "转盘",
"tariffs": "套餐", "tariffs": "套餐",
"servers": "服务器", "servers": "小队",
"broadcasts": "群发", "broadcasts": "群发",
"emailTemplates": "邮件模板", "emailTemplates": "邮件模板",
"paymentMethods": "支付方式", "paymentMethods": "支付方式",

View File

@@ -186,14 +186,14 @@ export default function AdminServers() {
onClick={() => toggleMutation.mutate(server.id)} onClick={() => toggleMutation.mutate(server.id)}
className={`rounded-lg p-2 transition-colors ${ className={`rounded-lg p-2 transition-colors ${
server.is_available server.is_available
? 'bg-success-500/20 text-success-400 hover:bg-success-500/30' ? 'bg-error-500/20 text-error-400 hover:bg-error-500/30'
: 'bg-dark-700 text-dark-400 hover:bg-dark-600' : 'bg-success-500/20 text-success-400 hover:bg-success-500/30'
}`} }`}
title={ title={
server.is_available ? t('admin.servers.disable') : t('admin.servers.enable') server.is_available ? t('admin.servers.disable') : t('admin.servers.enable')
} }
> >
{server.is_available ? <CheckIcon /> : <XIcon />} {server.is_available ? <XIcon /> : <CheckIcon />}
</button> </button>
{/* Toggle Trial */} {/* Toggle Trial */}
@@ -201,7 +201,7 @@ export default function AdminServers() {
onClick={() => toggleTrialMutation.mutate(server.id)} onClick={() => toggleTrialMutation.mutate(server.id)}
className={`rounded-lg p-2 transition-colors ${ className={`rounded-lg p-2 transition-colors ${
server.is_trial_eligible server.is_trial_eligible
? 'bg-accent-500/20 text-accent-400 hover:bg-accent-500/30' ? 'bg-warning-500/20 text-warning-400 hover:bg-warning-500/30'
: 'bg-dark-700 text-dark-400 hover:bg-dark-600' : 'bg-dark-700 text-dark-400 hover:bg-dark-600'
}`} }`}
title={t('admin.servers.toggleTrial')} title={t('admin.servers.toggleTrial')}

View File

@@ -1658,7 +1658,10 @@ export default function AdminUserDetail() {
.filter((tariffItem) => { .filter((tariffItem) => {
const purchasedIds = new Set( const purchasedIds = new Set(
userSubscriptions userSubscriptions
.filter((s) => s.is_active || s.status === 'trial') .filter(
(s) =>
s.is_active || s.status === 'trial' || s.status === 'limited',
)
.map((s) => s.tariff_id), .map((s) => s.tariff_id),
); );
return !purchasedIds.has(tariffItem.id); return !purchasedIds.has(tariffItem.id);

View File

@@ -516,35 +516,14 @@ function BuyTabContent({
const [selectedSubOption, setSelectedSubOption] = useState<string | null>(null); const [selectedSubOption, setSelectedSubOption] = useState<string | null>(null);
const [submitError, setSubmitError] = useState<string | null>(null); const [submitError, setSubmitError] = useState<string | null>(null);
// Collect ALL unique periods across ALL tariffs
const allPeriods = useMemo(() => {
const periodMap = new Map<number, GiftTariffPeriod>();
for (const tariff of config.tariffs) {
for (const period of tariff.periods) {
if (!periodMap.has(period.days)) {
periodMap.set(period.days, period);
}
}
}
return Array.from(periodMap.values()).sort((a, b) => a.days - b.days);
}, [config]);
// Filter tariffs to only those that have the selected period
const visibleTariffs = useMemo(() => {
if (!selectedPeriodDays) return config.tariffs;
return config.tariffs.filter((tariff) =>
tariff.periods.some((p) => p.days === selectedPeriodDays),
);
}, [config, selectedPeriodDays]);
// Auto-select first tariff, period, method on config load // Auto-select first tariff, period, method on config load
useEffect(() => { useEffect(() => {
if (allPeriods.length > 0 && selectedPeriodDays === null) { if (config.tariffs.length > 0 && selectedTariffId === null) {
setSelectedPeriodDays(allPeriods[0].days); const firstTariff = config.tariffs[0];
} setSelectedTariffId(firstTariff.id);
if (firstTariff.periods.length > 0 && selectedPeriodDays === null) {
if (visibleTariffs.length > 0 && selectedTariffId === null) { setSelectedPeriodDays(firstTariff.periods[0].days);
setSelectedTariffId(visibleTariffs[0].id); }
} }
if (config.payment_methods.length > 0 && selectedMethod === null) { if (config.payment_methods.length > 0 && selectedMethod === null) {
@@ -556,16 +535,19 @@ function BuyTabContent({
setSelectedSubOption(null); setSelectedSubOption(null);
} }
} }
}, [config, allPeriods, visibleTariffs, selectedTariffId, selectedPeriodDays, selectedMethod]); }, [config, selectedTariffId, selectedPeriodDays, selectedMethod]);
// When period changes, auto-select first visible tariff if current is hidden // When tariff changes, auto-select its first period
useEffect(() => { useEffect(() => {
if (!visibleTariffs.length) return; if (!selectedTariffId) return;
const currentVisible = visibleTariffs.find((tariff) => tariff.id === selectedTariffId); const tariff = config.tariffs.find((t) => t.id === selectedTariffId);
if (!currentVisible) { if (tariff && tariff.periods.length > 0) {
setSelectedTariffId(visibleTariffs[0].id); const hasCurrent = tariff.periods.some((p) => p.days === selectedPeriodDays);
if (!hasCurrent) {
setSelectedPeriodDays(tariff.periods[0].days);
}
} }
}, [visibleTariffs, selectedTariffId]); }, [selectedTariffId, config.tariffs, selectedPeriodDays]);
// Derived data // Derived data
const selectedTariff = useMemo( const selectedTariff = useMemo(
@@ -661,15 +643,15 @@ function BuyTabContent({
return `${t('gift.fromBalance')} (${formatPrice(config.balance_kopeks)})`; return `${t('gift.fromBalance')} (${formatPrice(config.balance_kopeks)})`;
}, [config, t]); }, [config, t]);
const showTariffCards = visibleTariffs.length > 1; const showTariffCards = config.tariffs.length > 1;
// Periods for the selected tariff (for period cards) // Periods for the selected tariff (for period cards)
const periodsForDisplay = useMemo(() => { const periodsForDisplay = useMemo(() => {
if (selectedTariff) { if (selectedTariff) {
return [...selectedTariff.periods].sort((a, b) => a.days - b.days); return [...selectedTariff.periods].sort((a, b) => a.days - b.days);
} }
return allPeriods; return [];
}, [selectedTariff, allPeriods]); }, [selectedTariff]);
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -680,7 +662,7 @@ function BuyTabContent({
{t('gift.selectTariff')} {t('gift.selectTariff')}
</h2> </h2>
<div role="radiogroup" aria-label={t('gift.chooseTariff')} className="space-y-2"> <div role="radiogroup" aria-label={t('gift.chooseTariff')} className="space-y-2">
{visibleTariffs.map((tariff) => ( {config.tariffs.map((tariff) => (
<TariffCard <TariffCard
key={tariff.id} key={tariff.id}
tariff={tariff} tariff={tariff}

View File

@@ -957,7 +957,7 @@ export default function Subscription() {
<div className="mb-5"> <div className="mb-5">
<CountdownTimer <CountdownTimer
endDate={subscription.end_date} endDate={subscription.end_date}
isActive={subscription.is_active} isActive={subscription.is_active || subscription.is_limited}
glassColors={g} glassColors={g}
/> />
</div> </div>
@@ -1316,68 +1316,25 @@ export default function Subscription() {
<PurchaseCTAButton subscription={subscription} isMultiTariff={isMultiTariff} /> <PurchaseCTAButton subscription={subscription} isMultiTariff={isMultiTariff} />
{/* Delete expired subscription */} {/* Delete expired subscription */}
{isMultiTariff && subscription && !subscription.is_active && !subscription.is_trial && ( {isMultiTariff &&
<div className="space-y-3"> subscription &&
{!showDeleteSheet ? ( !subscription.is_active &&
<button !subscription.is_trial &&
onClick={async () => { !subscription.is_limited && (
if (platform === 'telegram') { <div className="space-y-3">
const confirmed = await destructiveConfirm( {!showDeleteSheet ? (
t( <button
'subscription.deleteWarning', onClick={async () => {
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны.', if (platform === 'telegram') {
), const confirmed = await destructiveConfirm(
t('subscription.confirmDelete', 'Да, удалить'), t(
t('subscription.deleteTitle', 'Удалить подписку?'), 'subscription.deleteWarning',
); 'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны.',
if (!confirmed) return; ),
setDeleteLoading(true); t('subscription.confirmDelete', 'Да, удалить'),
try { t('subscription.deleteTitle', 'Удалить подписку?'),
await subscriptionApi.deleteSubscription(subscription.id); );
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] }); if (!confirmed) return;
navigate('/subscriptions', { replace: true });
} catch {
setDeleteLoading(false);
}
} else {
setShowDeleteSheet(true);
}
}}
disabled={deleteLoading}
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-red-400/20 bg-red-400/5 p-3.5 text-sm font-medium text-red-400 transition-colors hover:bg-red-400/10 disabled:opacity-50"
>
<svg
className="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
{t('subscription.delete', 'Удалить подписку')}
</button>
) : (
<div
className="rounded-2xl border border-red-400/20 p-4"
style={{ background: 'rgba(255,59,92,0.04)' }}
>
<div className="mb-3 text-sm font-semibold text-red-400">
{t('subscription.deleteTitle', 'Удалить подписку?')}
</div>
<div className="mb-4 text-xs" style={{ color: g.textSecondary }}>
{t(
'subscription.deleteWarning',
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны. Это действие нельзя отменить.',
)}
</div>
<div className="flex gap-2">
<button
onClick={async () => {
setDeleteLoading(true); setDeleteLoading(true);
try { try {
await subscriptionApi.deleteSubscription(subscription.id); await subscriptionApi.deleteSubscription(subscription.id);
@@ -1385,28 +1342,75 @@ export default function Subscription() {
navigate('/subscriptions', { replace: true }); navigate('/subscriptions', { replace: true });
} catch { } catch {
setDeleteLoading(false); setDeleteLoading(false);
setShowDeleteSheet(false);
} }
}} } else {
disabled={deleteLoading} setShowDeleteSheet(true);
className="flex-1 rounded-xl bg-red-500 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-red-600 disabled:opacity-50" }
}}
disabled={deleteLoading}
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-red-400/20 bg-red-400/5 p-3.5 text-sm font-medium text-red-400 transition-colors hover:bg-red-400/10 disabled:opacity-50"
>
<svg
className="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
> >
{deleteLoading <path
? t('common.processing', 'Удаление...') strokeLinecap="round"
: t('subscription.confirmDelete', 'Да, удалить')} strokeLinejoin="round"
</button> d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
<button />
onClick={() => setShowDeleteSheet(false)} </svg>
className="flex-1 rounded-xl border border-dark-700 py-2.5 text-sm font-medium transition-colors hover:bg-dark-700" {t('subscription.delete', 'Удалить подписку')}
style={{ color: g.textSecondary }} </button>
> ) : (
{t('common.cancel', 'Отмена')} <div
</button> className="rounded-2xl border border-red-400/20 p-4"
style={{ background: 'rgba(255,59,92,0.04)' }}
>
<div className="mb-3 text-sm font-semibold text-red-400">
{t('subscription.deleteTitle', 'Удалить подписку?')}
</div>
<div className="mb-4 text-xs" style={{ color: g.textSecondary }}>
{t(
'subscription.deleteWarning',
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны. Это действие нельзя отменить.',
)}
</div>
<div className="flex gap-2">
<button
onClick={async () => {
setDeleteLoading(true);
try {
await subscriptionApi.deleteSubscription(subscription.id);
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
navigate('/subscriptions', { replace: true });
} catch {
setDeleteLoading(false);
setShowDeleteSheet(false);
}
}}
disabled={deleteLoading}
className="flex-1 rounded-xl bg-red-500 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-red-600 disabled:opacity-50"
>
{deleteLoading
? t('common.processing', 'Удаление...')
: t('subscription.confirmDelete', 'Да, удалить')}
</button>
<button
onClick={() => setShowDeleteSheet(false)}
className="flex-1 rounded-xl border border-dark-700 py-2.5 text-sm font-medium transition-colors hover:bg-dark-700"
style={{ color: g.textSecondary }}
>
{t('common.cancel', 'Отмена')}
</button>
</div>
</div> </div>
</div> )}
)} </div>
</div> )}
)}
{/* Additional Options (Buy Devices) */} {/* Additional Options (Buy Devices) */}
{subscription && {subscription &&

View File

@@ -781,7 +781,7 @@ export default function SubscriptionPurchase() {
!isCurrentTariff && !isCurrentTariff &&
!subscription.is_trial && !subscription.is_trial &&
!isSubscriptionExpired && !isSubscriptionExpired &&
subscription.is_active; (subscription.is_active || subscription.is_limited);
const isLegacySubscription = const isLegacySubscription =
subscription && !subscription.is_trial && !subscription.tariff_id; subscription && !subscription.is_trial && !subscription.tariff_id;

View File

@@ -7,11 +7,28 @@ export function formatUptime(seconds: number): string {
return `${minutes}m`; return `${minutes}m`;
} }
export function formatPrice(kopeks: number): string { import i18next from 'i18next';
const LANG_CURRENCY_MAP: Record<string, { currency: string; locale: string; symbol: string }> = {
ru: { currency: 'RUB', locale: 'ru-RU', symbol: '₽' },
en: { currency: 'USD', locale: 'en-US', symbol: '$' },
zh: { currency: 'CNY', locale: 'zh-CN', symbol: '¥' },
fa: { currency: 'IRR', locale: 'fa-IR', symbol: '﷼' },
};
const DEFAULT_CURRENCY = { currency: 'RUB', locale: 'ru-RU', symbol: '₽' };
export function formatPrice(kopeks: number, lang?: string): string {
const resolvedLang = lang || i18next.language || 'ru';
const config = LANG_CURRENCY_MAP[resolvedLang] || DEFAULT_CURRENCY;
const rubles = kopeks / 100; const rubles = kopeks / 100;
return new Intl.NumberFormat('ru-RU', { try {
style: 'currency', return new Intl.NumberFormat(config.locale, {
currency: 'RUB', style: 'currency',
maximumFractionDigits: 0, currency: config.currency,
}).format(rubles); maximumFractionDigits: 0,
}).format(rubles);
} catch {
return `${Math.round(rubles)} ${config.symbol}`;
}
} }