diff --git a/src/components/dashboard/SubscriptionCardExpired.tsx b/src/components/dashboard/SubscriptionCardExpired.tsx index 499ab92..361e727 100644 --- a/src/components/dashboard/SubscriptionCardExpired.tsx +++ b/src/components/dashboard/SubscriptionCardExpired.tsx @@ -228,7 +228,7 @@ export default function SubscriptionCardExpired({ >
- {t('dashboard.expired.expiredDate')} + {isLimited ? t('dashboard.expired.activeUntil') : t('dashboard.expired.expiredDate')}
{formattedDate} @@ -280,7 +280,7 @@ export default function SubscriptionCardExpired({ > - {t('subscription.trafficLimited')} + {t('subscription.buyTraffic')} ) : ( <> diff --git a/src/components/subscription/PurchaseCTAButton.tsx b/src/components/subscription/PurchaseCTAButton.tsx index 1fa7b0f..a4c816a 100644 --- a/src/components/subscription/PurchaseCTAButton.tsx +++ b/src/components/subscription/PurchaseCTAButton.tsx @@ -15,7 +15,9 @@ export default function PurchaseCTAButton({ }: PurchaseCTAButtonProps) { 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 isDaily = subscription?.is_daily; diff --git a/src/components/subscription/SubscriptionListCard.tsx b/src/components/subscription/SubscriptionListCard.tsx index d92f7ff..48315af 100644 --- a/src/components/subscription/SubscriptionListCard.tsx +++ b/src/components/subscription/SubscriptionListCard.tsx @@ -82,7 +82,10 @@ export default function SubscriptionListCard({ }; 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 trafficLimit = subscription.traffic_limit_gb; const trafficUsed = subscription.traffic_used_gb; @@ -95,21 +98,25 @@ export default function SubscriptionListCard({ const trafficColor = trafficPercent >= 90 ? 'bg-red-400' : trafficPercent >= 70 ? 'bg-amber-400' : 'bg-emerald-400'; - const borderColor = isTrial - ? 'rgba(251,191,36,0.2)' - : isExpired - ? 'rgba(255,59,92,0.15)' - : g.cardBorder; + const isLimitedStatus = subscription.status === 'limited'; - const bgColor = isTrial - ? isDark - ? 'rgba(251,191,36,0.04)' - : 'rgba(251,191,36,0.03)' - : isExpired + const borderColor = + isTrial || isLimitedStatus + ? 'rgba(251,191,36,0.2)' + : isExpired + ? 'rgba(255,59,92,0.15)' + : g.cardBorder; + + const bgColor = + isTrial || isLimitedStatus ? isDark - ? 'rgba(255,59,92,0.04)' - : 'rgba(255,59,92,0.03)' - : g.cardBg; + ? 'rgba(251,191,36,0.04)' + : 'rgba(251,191,36,0.03)' + : isExpired + ? isDark + ? 'rgba(255,59,92,0.04)' + : 'rgba(255,59,92,0.03)' + : g.cardBg; return ( {/* Toggle Trial */} @@ -201,7 +201,7 @@ export default function AdminServers() { onClick={() => toggleTrialMutation.mutate(server.id)} className={`rounded-lg p-2 transition-colors ${ 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' }`} title={t('admin.servers.toggleTrial')} diff --git a/src/pages/AdminUserDetail.tsx b/src/pages/AdminUserDetail.tsx index 0455580..a5a80ae 100644 --- a/src/pages/AdminUserDetail.tsx +++ b/src/pages/AdminUserDetail.tsx @@ -1658,7 +1658,10 @@ export default function AdminUserDetail() { .filter((tariffItem) => { const purchasedIds = new Set( 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), ); return !purchasedIds.has(tariffItem.id); diff --git a/src/pages/GiftSubscription.tsx b/src/pages/GiftSubscription.tsx index 781e36d..b929905 100644 --- a/src/pages/GiftSubscription.tsx +++ b/src/pages/GiftSubscription.tsx @@ -516,35 +516,14 @@ function BuyTabContent({ const [selectedSubOption, setSelectedSubOption] = useState(null); const [submitError, setSubmitError] = useState(null); - // Collect ALL unique periods across ALL tariffs - const allPeriods = useMemo(() => { - const periodMap = new Map(); - 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 useEffect(() => { - if (allPeriods.length > 0 && selectedPeriodDays === null) { - setSelectedPeriodDays(allPeriods[0].days); - } - - if (visibleTariffs.length > 0 && selectedTariffId === null) { - setSelectedTariffId(visibleTariffs[0].id); + if (config.tariffs.length > 0 && selectedTariffId === null) { + const firstTariff = config.tariffs[0]; + setSelectedTariffId(firstTariff.id); + if (firstTariff.periods.length > 0 && selectedPeriodDays === null) { + setSelectedPeriodDays(firstTariff.periods[0].days); + } } if (config.payment_methods.length > 0 && selectedMethod === null) { @@ -556,16 +535,19 @@ function BuyTabContent({ 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(() => { - if (!visibleTariffs.length) return; - const currentVisible = visibleTariffs.find((tariff) => tariff.id === selectedTariffId); - if (!currentVisible) { - setSelectedTariffId(visibleTariffs[0].id); + if (!selectedTariffId) return; + const tariff = config.tariffs.find((t) => t.id === selectedTariffId); + if (tariff && tariff.periods.length > 0) { + const hasCurrent = tariff.periods.some((p) => p.days === selectedPeriodDays); + if (!hasCurrent) { + setSelectedPeriodDays(tariff.periods[0].days); + } } - }, [visibleTariffs, selectedTariffId]); + }, [selectedTariffId, config.tariffs, selectedPeriodDays]); // Derived data const selectedTariff = useMemo( @@ -661,15 +643,15 @@ function BuyTabContent({ return `${t('gift.fromBalance')} (${formatPrice(config.balance_kopeks)})`; }, [config, t]); - const showTariffCards = visibleTariffs.length > 1; + const showTariffCards = config.tariffs.length > 1; // Periods for the selected tariff (for period cards) const periodsForDisplay = useMemo(() => { if (selectedTariff) { return [...selectedTariff.periods].sort((a, b) => a.days - b.days); } - return allPeriods; - }, [selectedTariff, allPeriods]); + return []; + }, [selectedTariff]); return (
@@ -680,7 +662,7 @@ function BuyTabContent({ {t('gift.selectTariff')}
- {visibleTariffs.map((tariff) => ( + {config.tariffs.map((tariff) => (
@@ -1316,68 +1316,25 @@ export default function Subscription() { {/* Delete expired subscription */} - {isMultiTariff && subscription && !subscription.is_active && !subscription.is_trial && ( -
- {!showDeleteSheet ? ( - - ) : ( -
-
- {t('subscription.deleteTitle', 'Удалить подписку?')} -
-
- {t( - 'subscription.deleteWarning', - 'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны. Это действие нельзя отменить.', - )} -
-
- - + + + {t('subscription.delete', 'Удалить подписку')} + + ) : ( +
+
+ {t('subscription.deleteTitle', 'Удалить подписку?')} +
+
+ {t( + 'subscription.deleteWarning', + 'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны. Это действие нельзя отменить.', + )} +
+
+ + +
-
- )} -
- )} + )} +
+ )} {/* Additional Options (Buy Devices) */} {subscription && diff --git a/src/pages/SubscriptionPurchase.tsx b/src/pages/SubscriptionPurchase.tsx index b3a888c..8221ece 100644 --- a/src/pages/SubscriptionPurchase.tsx +++ b/src/pages/SubscriptionPurchase.tsx @@ -781,7 +781,7 @@ export default function SubscriptionPurchase() { !isCurrentTariff && !subscription.is_trial && !isSubscriptionExpired && - subscription.is_active; + (subscription.is_active || subscription.is_limited); const isLegacySubscription = subscription && !subscription.is_trial && !subscription.tariff_id; diff --git a/src/utils/format.ts b/src/utils/format.ts index f084109..ee42e2f 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -7,11 +7,28 @@ export function formatUptime(seconds: number): string { return `${minutes}m`; } -export function formatPrice(kopeks: number): string { +import i18next from 'i18next'; + +const LANG_CURRENCY_MAP: Record = { + 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; - return new Intl.NumberFormat('ru-RU', { - style: 'currency', - currency: 'RUB', - maximumFractionDigits: 0, - }).format(rubles); + try { + return new Intl.NumberFormat(config.locale, { + style: 'currency', + currency: config.currency, + maximumFractionDigits: 0, + }).format(rubles); + } catch { + return `${Math.round(rubles)} ${config.symbol}`; + } }