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 Twemoji from 'react-twemoji';
|
||||||
import { SwitchTariffSheet } from '../components/subscription/sheets/SwitchTariffSheet';
|
import { SwitchTariffSheet } from '../components/subscription/sheets/SwitchTariffSheet';
|
||||||
import { TariffPurchaseForm } from '../components/subscription/purchase/TariffPurchaseForm';
|
import { TariffPurchaseForm } from '../components/subscription/purchase/TariffPurchaseForm';
|
||||||
|
import { TariffPickerGrid } from '../components/subscription/purchase/TariffPickerGrid';
|
||||||
import { getErrorMessage, type PurchaseStep } from '../utils/subscriptionHelpers';
|
import { getErrorMessage, type PurchaseStep } from '../utils/subscriptionHelpers';
|
||||||
|
|
||||||
export default function SubscriptionPurchase() {
|
export default function SubscriptionPurchase() {
|
||||||
@@ -436,313 +437,18 @@ export default function SubscriptionPurchase() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{!showTariffPurchase ? (
|
{!showTariffPurchase ? (
|
||||||
<>
|
<TariffPickerGrid
|
||||||
{/* Promo group discount banner */}
|
tariffs={tariffs}
|
||||||
{tariffs.some((tariff) => tariff.promo_group_name) && (
|
subscription={subscription}
|
||||||
<div className="mb-4 flex items-center gap-3 rounded-xl border border-success-500/30 bg-success-500/10 p-3">
|
purchaseOptions={purchaseOptions}
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-success-500/20 text-success-400">
|
isTariffsMode={isTariffsMode}
|
||||||
<svg
|
isMultiTariff={isMultiTariff}
|
||||||
className="h-5 w-5"
|
onSelectTariff={(tariff) => {
|
||||||
fill="none"
|
setSelectedTariff(tariff);
|
||||||
viewBox="0 0 24 24"
|
setShowTariffPurchase(true);
|
||||||
stroke="currentColor"
|
}}
|
||||||
strokeWidth={2}
|
onSwitchTariff={(tariffId) => setSwitchTariffId(tariffId)}
|
||||||
>
|
/>
|
||||||
<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={() => {
|
|
||||||
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>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
selectedTariff && (
|
selectedTariff && (
|
||||||
/* Tariff Purchase Form (extracted into its own component) */
|
/* Tariff Purchase Form (extracted into its own component) */
|
||||||
|
|||||||
Reference in New Issue
Block a user