mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
refactor(purchase): extract TariffPickerGrid from god page
Move the tariff selection surface (promo-group banner, all-purchased empty state, and the 1/2-col tariff cards grid with per-tariff CTAs) into src/components/subscription/purchase/TariffPickerGrid.tsx. The grid is pure presentation — it owns no queries or mutations. Two callbacks back into the parent: - onSelectTariff(tariff) → parent sets selectedTariff + showForm - onSwitchTariff(tariffId) → parent sets switchTariffId Internal helpers (formatPrice via useCurrency, applyPromoDiscount via usePromoDiscount, glass tokens via useTheme + getGlassColors, navigate for the 'all purchased' back-to-list button) all derive inside the component — nothing threaded as props beyond data. SubscriptionPurchase.tsx: 1202 → 908 lines (-294).
This commit is contained in:
347
src/components/subscription/purchase/TariffPickerGrid.tsx
Normal file
347
src/components/subscription/purchase/TariffPickerGrid.tsx
Normal file
@@ -0,0 +1,347 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useTheme } from '../../../hooks/useTheme';
|
||||
import { useCurrency } from '../../../hooks/useCurrency';
|
||||
import { usePromoDiscount } from '../../../hooks/usePromoDiscount';
|
||||
import { getGlassColors } from '../../../utils/glassTheme';
|
||||
import type { Tariff, Subscription, PurchaseOptions } from '../../../types';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// TariffPickerGrid
|
||||
//
|
||||
// The tariff selection surface inside SubscriptionPurchase. Renders:
|
||||
// - an optional promo-group banner when any tariff carries a
|
||||
// promo_group_name
|
||||
// - the "all tariffs purchased" empty state (multi-tariff mode)
|
||||
// - the grid itself (1 col mobile, 2 cols sm+) with promo prices,
|
||||
// per-tariff CTAs differentiated by user state (extend / switch /
|
||||
// purchase / legacy renewal)
|
||||
//
|
||||
// Owns nothing — pure presentation that calls back into the parent
|
||||
// for selection (`onSelectTariff`) and switch (`onSwitchTariff`).
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface TariffPickerGridProps {
|
||||
tariffs: Tariff[];
|
||||
subscription: Subscription | null;
|
||||
purchaseOptions: PurchaseOptions | undefined;
|
||||
isTariffsMode: boolean;
|
||||
isMultiTariff: boolean;
|
||||
onSelectTariff: (tariff: Tariff) => void;
|
||||
onSwitchTariff: (tariffId: number) => void;
|
||||
}
|
||||
|
||||
export function TariffPickerGrid({
|
||||
tariffs,
|
||||
subscription,
|
||||
purchaseOptions,
|
||||
isTariffsMode,
|
||||
isMultiTariff,
|
||||
onSelectTariff,
|
||||
onSwitchTariff,
|
||||
}: TariffPickerGridProps) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { isDark } = useTheme();
|
||||
const g = getGlassColors(isDark);
|
||||
const { formatAmount, currencySymbol } = useCurrency();
|
||||
const { applyPromoDiscount } = usePromoDiscount();
|
||||
|
||||
const formatPrice = (kopeks: number) =>
|
||||
kopeks === 0
|
||||
? t('subscription.free', 'Бесплатно')
|
||||
: `${formatAmount(kopeks / 100)} ${currencySymbol}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Promo group discount banner */}
|
||||
{tariffs.some((tariff) => tariff.promo_group_name) && (
|
||||
<div className="mb-4 flex items-center gap-3 rounded-xl border border-success-500/30 bg-success-500/10 p-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-success-500/20 text-success-400">
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-success-400">
|
||||
{t('subscription.promoGroup.title', {
|
||||
name: tariffs.find((tariff) => tariff.promo_group_name)?.promo_group_name,
|
||||
})}
|
||||
</div>
|
||||
<div className="text-xs text-dark-400">
|
||||
{t('subscription.promoGroup.personalDiscountsApplied')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tariff Grid */}
|
||||
{isMultiTariff &&
|
||||
purchaseOptions &&
|
||||
'all_tariffs_purchased' in purchaseOptions &&
|
||||
purchaseOptions.all_tariffs_purchased && (
|
||||
<div
|
||||
className="rounded-2xl border p-6 text-center"
|
||||
style={{ background: g.cardBg, borderColor: g.cardBorder }}
|
||||
>
|
||||
<div className="mb-2 text-3xl">✅</div>
|
||||
<h3 className="mb-1 text-lg font-semibold" style={{ color: g.text }}>
|
||||
{t('subscription.allTariffsPurchased', 'Все тарифы подключены')}
|
||||
</h3>
|
||||
<p className="mb-4 text-sm" style={{ color: g.textSecondary }}>
|
||||
{t(
|
||||
'subscription.allTariffsPurchasedDesc',
|
||||
'Вы уже приобрели все доступные тарифы. Продлить подписку можно на странице тарифа.',
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/subscriptions')}
|
||||
className="rounded-xl bg-accent-500 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600"
|
||||
>
|
||||
{t('subscription.backToList', 'Мои подписки')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{[...tariffs]
|
||||
.filter((tariff) => {
|
||||
// In multi-tariff mode: hide already purchased tariffs
|
||||
if (isMultiTariff && tariff.is_purchased) return false;
|
||||
if (subscription?.is_trial && tariff.name.toLowerCase().includes('trial')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aIsCurrent = a.is_current || a.id === subscription?.tariff_id;
|
||||
const bIsCurrent = b.is_current || b.id === subscription?.tariff_id;
|
||||
if (aIsCurrent && !bIsCurrent) return -1;
|
||||
if (!aIsCurrent && bIsCurrent) return 1;
|
||||
return 0;
|
||||
})
|
||||
.map((tariff) => {
|
||||
const isCurrentTariff = tariff.is_current || tariff.id === subscription?.tariff_id;
|
||||
const isSubscriptionExpired =
|
||||
isTariffsMode &&
|
||||
purchaseOptions &&
|
||||
'subscription_is_expired' in purchaseOptions &&
|
||||
purchaseOptions.subscription_is_expired === true;
|
||||
const canSwitch =
|
||||
!isMultiTariff &&
|
||||
subscription &&
|
||||
subscription.tariff_id &&
|
||||
!isCurrentTariff &&
|
||||
!subscription.is_trial &&
|
||||
!isSubscriptionExpired &&
|
||||
(subscription.is_active || subscription.is_limited);
|
||||
const isLegacySubscription =
|
||||
subscription && !subscription.is_trial && !subscription.tariff_id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tariff.id}
|
||||
className={`bento-card-hover p-5 text-left transition-all ${
|
||||
isCurrentTariff ? 'bento-card-glow border-accent-500' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="mb-3 flex items-start justify-between">
|
||||
<div>
|
||||
<div className="text-lg font-semibold text-dark-100">{tariff.name}</div>
|
||||
{tariff.description && (
|
||||
<div className="mt-1 whitespace-pre-line text-sm text-dark-400">
|
||||
{tariff.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCurrentTariff && (
|
||||
<span className="badge-success text-xs">{t('subscription.currentTariff')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-accent-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
||||
/>
|
||||
</svg>
|
||||
<span className="font-medium text-dark-200">{tariff.traffic_limit_label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<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>
|
||||
<span className="text-dark-300">
|
||||
{tariff.device_limit === 0
|
||||
? '∞'
|
||||
: t('subscription.devices', { count: tariff.device_limit })}
|
||||
</span>
|
||||
</div>
|
||||
{tariff.traffic_reset_mode && tariff.traffic_reset_mode !== 'NO_RESET' && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<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.182M2.985 19.644l3.181-3.182"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-dark-300">
|
||||
{t(`subscription.trafficReset.${tariff.traffic_reset_mode}`)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Price info */}
|
||||
<div className="mt-3 border-t border-dark-700/50 pt-3 text-sm text-dark-400">
|
||||
{(() => {
|
||||
const dailyPrice =
|
||||
tariff.daily_price_kopeks ?? tariff.price_per_day_kopeks ?? 0;
|
||||
const originalDailyPrice = tariff.original_daily_price_kopeks || 0;
|
||||
if (dailyPrice > 0 || originalDailyPrice > 0) {
|
||||
const promoDaily = applyPromoDiscount(
|
||||
dailyPrice,
|
||||
originalDailyPrice > dailyPrice ? originalDailyPrice : undefined,
|
||||
);
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="font-medium text-accent-400">
|
||||
{formatPrice(promoDaily.price)}
|
||||
</span>
|
||||
{promoDaily.original && promoDaily.original > promoDaily.price && (
|
||||
<span className="text-xs text-dark-500 line-through">
|
||||
{formatPrice(promoDaily.original)}
|
||||
</span>
|
||||
)}
|
||||
<span>{t('subscription.tariff.perDay')}</span>
|
||||
{promoDaily.percent && promoDaily.percent > 0 && (
|
||||
<span
|
||||
className={`rounded px-1.5 py-0.5 text-xs ${
|
||||
promoDaily.isPromoGroup
|
||||
? 'bg-success-500/20 text-success-400'
|
||||
: 'bg-warning-500/20 text-warning-400'
|
||||
}`}
|
||||
>
|
||||
-{promoDaily.percent}%
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (tariff.periods.length > 0) {
|
||||
const firstPeriod = tariff.periods[0];
|
||||
const promoPeriod = applyPromoDiscount(
|
||||
firstPeriod?.price_kopeks || 0,
|
||||
firstPeriod?.original_price_kopeks,
|
||||
);
|
||||
return (
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
<span>{t('subscription.from')}</span>
|
||||
<span className="font-medium text-accent-400">
|
||||
{formatPrice(promoPeriod.price)}
|
||||
</span>
|
||||
{promoPeriod.original && promoPeriod.original > promoPeriod.price && (
|
||||
<span className="text-xs text-dark-500 line-through">
|
||||
{formatPrice(promoPeriod.original)}
|
||||
</span>
|
||||
)}
|
||||
{promoPeriod.percent && promoPeriod.percent > 0 && (
|
||||
<span
|
||||
className={`rounded px-1.5 py-0.5 text-xs ${
|
||||
promoPeriod.isPromoGroup
|
||||
? 'bg-success-500/20 text-success-400'
|
||||
: 'bg-warning-500/20 text-warning-400'
|
||||
}`}
|
||||
>
|
||||
-{promoPeriod.percent}%
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="font-medium text-accent-400">
|
||||
{t('subscription.tariff.flexiblePayment')}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="mt-4 flex gap-2">
|
||||
{isCurrentTariff ? (
|
||||
subscription?.is_daily ? (
|
||||
<div className="flex-1 py-2 text-center text-sm text-dark-500">
|
||||
{t('subscription.currentTariff')}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onSelectTariff(tariff)}
|
||||
className="btn-primary flex-1 py-2 text-sm"
|
||||
>
|
||||
{t('subscription.extend')}
|
||||
</button>
|
||||
)
|
||||
) : isLegacySubscription ? (
|
||||
<button
|
||||
onClick={() => onSelectTariff(tariff)}
|
||||
className="btn-primary flex-1 py-2 text-sm"
|
||||
>
|
||||
{t('subscription.tariff.selectForRenewal')}
|
||||
</button>
|
||||
) : canSwitch ? (
|
||||
<button
|
||||
onClick={() => onSwitchTariff(tariff.id)}
|
||||
className="btn-secondary flex-1 py-2 text-sm"
|
||||
>
|
||||
{t('subscription.switchTariff.switch')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onSelectTariff(tariff)}
|
||||
className="btn-primary flex-1 py-2 text-sm"
|
||||
>
|
||||
{t('subscription.purchase')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { CheckIcon } from '../components/icons';
|
||||
import Twemoji from 'react-twemoji';
|
||||
import { SwitchTariffSheet } from '../components/subscription/sheets/SwitchTariffSheet';
|
||||
import { TariffPurchaseForm } from '../components/subscription/purchase/TariffPurchaseForm';
|
||||
import { TariffPickerGrid } from '../components/subscription/purchase/TariffPickerGrid';
|
||||
import { getErrorMessage, type PurchaseStep } from '../utils/subscriptionHelpers';
|
||||
|
||||
export default function SubscriptionPurchase() {
|
||||
@@ -436,313 +437,18 @@ export default function SubscriptionPurchase() {
|
||||
/>
|
||||
|
||||
{!showTariffPurchase ? (
|
||||
<>
|
||||
{/* Promo group discount banner */}
|
||||
{tariffs.some((tariff) => tariff.promo_group_name) && (
|
||||
<div className="mb-4 flex items-center gap-3 rounded-xl border border-success-500/30 bg-success-500/10 p-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-success-500/20 text-success-400">
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-success-400">
|
||||
{t('subscription.promoGroup.yourGroup', {
|
||||
name: tariffs.find((tariff) => tariff.promo_group_name)?.promo_group_name,
|
||||
})}
|
||||
</div>
|
||||
<div className="text-xs text-dark-400">
|
||||
{t('subscription.promoGroup.personalDiscountsApplied')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tariff Grid */}
|
||||
{isMultiTariff &&
|
||||
purchaseOptions &&
|
||||
'all_tariffs_purchased' in purchaseOptions &&
|
||||
purchaseOptions.all_tariffs_purchased && (
|
||||
<div
|
||||
className="rounded-2xl border p-6 text-center"
|
||||
style={{ background: g.cardBg, borderColor: g.cardBorder }}
|
||||
>
|
||||
<div className="mb-2 text-3xl">✅</div>
|
||||
<h3 className="mb-1 text-lg font-semibold" style={{ color: g.text }}>
|
||||
{t('subscription.allTariffsPurchased', 'Все тарифы подключены')}
|
||||
</h3>
|
||||
<p className="mb-4 text-sm" style={{ color: g.textSecondary }}>
|
||||
{t(
|
||||
'subscription.allTariffsPurchasedDesc',
|
||||
'Вы уже приобрели все доступные тарифы. Продлить подписку можно на странице тарифа.',
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/subscriptions')}
|
||||
className="rounded-xl bg-accent-500 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600"
|
||||
>
|
||||
{t('subscription.backToList', 'Мои подписки')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{[...tariffs]
|
||||
.filter((tariff) => {
|
||||
// In multi-tariff mode: hide already purchased tariffs
|
||||
if (isMultiTariff && tariff.is_purchased) return false;
|
||||
if (subscription?.is_trial && tariff.name.toLowerCase().includes('trial')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aIsCurrent = a.is_current || a.id === subscription?.tariff_id;
|
||||
const bIsCurrent = b.is_current || b.id === subscription?.tariff_id;
|
||||
if (aIsCurrent && !bIsCurrent) return -1;
|
||||
if (!aIsCurrent && bIsCurrent) return 1;
|
||||
return 0;
|
||||
})
|
||||
.map((tariff) => {
|
||||
const isCurrentTariff =
|
||||
tariff.is_current || tariff.id === subscription?.tariff_id;
|
||||
const isSubscriptionExpired =
|
||||
isTariffsMode &&
|
||||
purchaseOptions &&
|
||||
'subscription_is_expired' in purchaseOptions &&
|
||||
purchaseOptions.subscription_is_expired === true;
|
||||
const canSwitch =
|
||||
!isMultiTariff &&
|
||||
subscription &&
|
||||
subscription.tariff_id &&
|
||||
!isCurrentTariff &&
|
||||
!subscription.is_trial &&
|
||||
!isSubscriptionExpired &&
|
||||
(subscription.is_active || subscription.is_limited);
|
||||
const isLegacySubscription =
|
||||
subscription && !subscription.is_trial && !subscription.tariff_id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tariff.id}
|
||||
className={`bento-card-hover p-5 text-left transition-all ${
|
||||
isCurrentTariff ? 'bento-card-glow border-accent-500' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="mb-3 flex items-start justify-between">
|
||||
<div>
|
||||
<div className="text-lg font-semibold text-dark-100">{tariff.name}</div>
|
||||
{tariff.description && (
|
||||
<div className="mt-1 whitespace-pre-line text-sm text-dark-400">
|
||||
{tariff.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCurrentTariff && (
|
||||
<span className="badge-success text-xs">
|
||||
{t('subscription.currentTariff')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-accent-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
||||
/>
|
||||
</svg>
|
||||
<span className="font-medium text-dark-200">
|
||||
{tariff.traffic_limit_label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<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>
|
||||
<span className="text-dark-300">
|
||||
{tariff.device_limit === 0
|
||||
? '∞'
|
||||
: t('subscription.devices', { count: tariff.device_limit })}
|
||||
</span>
|
||||
</div>
|
||||
{tariff.traffic_reset_mode &&
|
||||
tariff.traffic_reset_mode !== 'NO_RESET' && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<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.182M2.985 19.644l3.181-3.182"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-dark-300">
|
||||
{t(`subscription.trafficReset.${tariff.traffic_reset_mode}`)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Price info */}
|
||||
<div className="mt-3 border-t border-dark-700/50 pt-3 text-sm text-dark-400">
|
||||
{(() => {
|
||||
const dailyPrice =
|
||||
tariff.daily_price_kopeks ?? tariff.price_per_day_kopeks ?? 0;
|
||||
const originalDailyPrice = tariff.original_daily_price_kopeks || 0;
|
||||
if (dailyPrice > 0 || originalDailyPrice > 0) {
|
||||
const promoDaily = applyPromoDiscount(
|
||||
dailyPrice,
|
||||
originalDailyPrice > dailyPrice ? originalDailyPrice : undefined,
|
||||
);
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="font-medium text-accent-400">
|
||||
{formatPrice(promoDaily.price)}
|
||||
</span>
|
||||
{promoDaily.original &&
|
||||
promoDaily.original > promoDaily.price && (
|
||||
<span className="text-xs text-dark-500 line-through">
|
||||
{formatPrice(promoDaily.original)}
|
||||
</span>
|
||||
)}
|
||||
<span>{t('subscription.tariff.perDay')}</span>
|
||||
{promoDaily.percent && promoDaily.percent > 0 && (
|
||||
<span
|
||||
className={`rounded px-1.5 py-0.5 text-xs ${
|
||||
promoDaily.isPromoGroup
|
||||
? 'bg-success-500/20 text-success-400'
|
||||
: 'bg-warning-500/20 text-warning-400'
|
||||
}`}
|
||||
>
|
||||
-{promoDaily.percent}%
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (tariff.periods.length > 0) {
|
||||
const firstPeriod = tariff.periods[0];
|
||||
const promoPeriod = applyPromoDiscount(
|
||||
firstPeriod?.price_kopeks || 0,
|
||||
firstPeriod?.original_price_kopeks,
|
||||
);
|
||||
return (
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
<span>{t('subscription.from')}</span>
|
||||
<span className="font-medium text-accent-400">
|
||||
{formatPrice(promoPeriod.price)}
|
||||
</span>
|
||||
{promoPeriod.original &&
|
||||
promoPeriod.original > promoPeriod.price && (
|
||||
<span className="text-xs text-dark-500 line-through">
|
||||
{formatPrice(promoPeriod.original)}
|
||||
</span>
|
||||
)}
|
||||
{promoPeriod.percent && promoPeriod.percent > 0 && (
|
||||
<span
|
||||
className={`rounded px-1.5 py-0.5 text-xs ${
|
||||
promoPeriod.isPromoGroup
|
||||
? 'bg-success-500/20 text-success-400'
|
||||
: 'bg-warning-500/20 text-warning-400'
|
||||
}`}
|
||||
>
|
||||
-{promoPeriod.percent}%
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="font-medium text-accent-400">
|
||||
{t('subscription.tariff.flexiblePayment')}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="mt-4 flex gap-2">
|
||||
{isCurrentTariff ? (
|
||||
subscription?.is_daily ? (
|
||||
<div className="flex-1 py-2 text-center text-sm text-dark-500">
|
||||
{t('subscription.currentTariff')}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
<TariffPickerGrid
|
||||
tariffs={tariffs}
|
||||
subscription={subscription}
|
||||
purchaseOptions={purchaseOptions}
|
||||
isTariffsMode={isTariffsMode}
|
||||
isMultiTariff={isMultiTariff}
|
||||
onSelectTariff={(tariff) => {
|
||||
setSelectedTariff(tariff);
|
||||
setShowTariffPurchase(true);
|
||||
}}
|
||||
className="btn-primary flex-1 py-2 text-sm"
|
||||
>
|
||||
{t('subscription.extend')}
|
||||
</button>
|
||||
)
|
||||
) : isLegacySubscription ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedTariff(tariff);
|
||||
setShowTariffPurchase(true);
|
||||
}}
|
||||
className="btn-primary flex-1 py-2 text-sm"
|
||||
>
|
||||
{t('subscription.tariff.selectForRenewal')}
|
||||
</button>
|
||||
) : canSwitch ? (
|
||||
<button
|
||||
onClick={() => setSwitchTariffId(tariff.id)}
|
||||
className="btn-secondary flex-1 py-2 text-sm"
|
||||
>
|
||||
{t('subscription.switchTariff.switch')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedTariff(tariff);
|
||||
setShowTariffPurchase(true);
|
||||
}}
|
||||
className="btn-primary flex-1 py-2 text-sm"
|
||||
>
|
||||
{t('subscription.purchase')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
onSwitchTariff={(tariffId) => setSwitchTariffId(tariffId)}
|
||||
/>
|
||||
) : (
|
||||
selectedTariff && (
|
||||
/* Tariff Purchase Form (extracted into its own component) */
|
||||
|
||||
Reference in New Issue
Block a user