mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
refactor: extract purchase/renewal flow to separate page
Move ~1900 lines of purchase/renewal logic from Subscription.tsx into a new SubscriptionPurchase.tsx page at /subscription/purchase. - Create SubscriptionPurchase.tsx with tariffs grid, classic mode wizard, switch preview modal, and promo handling - Create PurchaseCTAButton.tsx with animated gradient border, state-aware colors and context-aware text - Create subscriptionHelpers.ts with shared utilities - Add CopyIcon to shared icons module - Register /subscription/purchase route with lazy loading - Update SubscriptionCardExpired and PromoOffersSection navigators - Add CTA translation keys to ru.json and en.json - Remove all scrollToExtend references
This commit is contained in:
11
src/App.tsx
11
src/App.tsx
@@ -26,6 +26,7 @@ import Dashboard from './pages/Dashboard';
|
||||
|
||||
// User pages - lazy load
|
||||
const Subscription = lazy(() => import('./pages/Subscription'));
|
||||
const SubscriptionPurchase = lazy(() => import('./pages/SubscriptionPurchase'));
|
||||
const Balance = lazy(() => import('./pages/Balance'));
|
||||
const Referral = lazy(() => import('./pages/Referral'));
|
||||
const Support = lazy(() => import('./pages/Support'));
|
||||
@@ -203,6 +204,16 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/subscription/purchase"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<LazyPage>
|
||||
<SubscriptionPurchase />
|
||||
</LazyPage>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/balance"
|
||||
element={
|
||||
|
||||
@@ -156,7 +156,7 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
|
||||
};
|
||||
|
||||
const handleUseNow = () => {
|
||||
navigate('/subscription', { state: { scrollToExtend: true } });
|
||||
navigate('/subscription/purchase');
|
||||
};
|
||||
|
||||
const handleDeactivateClick = () => {
|
||||
|
||||
@@ -139,8 +139,7 @@ export default function SubscriptionCardExpired({ subscription }: SubscriptionCa
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2.5">
|
||||
<Link
|
||||
to="/subscription"
|
||||
state={{ scrollToExtend: true }}
|
||||
to="/subscription/purchase"
|
||||
className="flex flex-1 items-center justify-center rounded-[14px] py-3.5 text-[15px] font-semibold tracking-tight text-white transition-all duration-300"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #FF3B5C, #FF6B35)',
|
||||
|
||||
@@ -242,6 +242,22 @@ export const CheckIcon = ({ className }: IconProps) => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CopyIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
className={cn('h-4 w-4', className)}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const XIcon = ({ className }: IconProps) => (
|
||||
<svg
|
||||
className={cn('h-4 w-4', className)}
|
||||
|
||||
93
src/components/subscription/PurchaseCTAButton.tsx
Normal file
93
src/components/subscription/PurchaseCTAButton.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Link } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HoverBorderGradient } from '../ui/hover-border-gradient';
|
||||
import type { Subscription } from '../../types';
|
||||
|
||||
interface PurchaseCTAButtonProps {
|
||||
subscription: Subscription | null;
|
||||
}
|
||||
|
||||
export default function PurchaseCTAButton({ subscription }: PurchaseCTAButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isExpired = !subscription || (!subscription.is_active && !subscription.is_trial);
|
||||
const isTrial = subscription?.is_trial;
|
||||
|
||||
const accentColor = isExpired ? '#FF3B5C' : '#3EDBB0';
|
||||
|
||||
const buttonText = isExpired
|
||||
? t('subscription.getSubscription')
|
||||
: isTrial
|
||||
? t('subscription.trialUpgrade.title')
|
||||
: t('subscription.extend');
|
||||
|
||||
const hintText = isExpired
|
||||
? t('subscription.cta.expiredHint')
|
||||
: isTrial
|
||||
? t('subscription.cta.trialHint')
|
||||
: t('subscription.cta.activeHint');
|
||||
|
||||
return (
|
||||
<Link to="/subscription/purchase" className="block">
|
||||
<HoverBorderGradient
|
||||
accentColor={accentColor}
|
||||
duration={4}
|
||||
className="group relative w-full cursor-pointer overflow-hidden rounded-2xl p-[1.5px]"
|
||||
>
|
||||
<div
|
||||
className="relative flex items-center justify-between rounded-[14px] px-5 py-4 transition-all duration-300"
|
||||
style={{
|
||||
background: isExpired
|
||||
? 'linear-gradient(135deg, rgba(255,59,92,0.08), rgba(255,107,53,0.06))'
|
||||
: 'linear-gradient(135deg, rgba(62,219,176,0.08), rgba(0,229,160,0.06))',
|
||||
}}
|
||||
>
|
||||
{/* Left: icon + text */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Sparkle icon */}
|
||||
<div
|
||||
className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl"
|
||||
style={{
|
||||
background: isExpired ? 'rgba(255,59,92,0.12)' : 'rgba(62,219,176,0.12)',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={accentColor}
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path 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>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[15px] font-semibold text-dark-50">{buttonText}</div>
|
||||
<div className="text-[12px] text-dark-50/40">{hintText}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: chevron */}
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
className="flex-shrink-0 text-dark-50/30 transition-transform duration-300 group-hover:translate-x-1"
|
||||
>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
</div>
|
||||
</HoverBorderGradient>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -318,6 +318,11 @@
|
||||
"price": "Price",
|
||||
"noSubscription": "You don't have an active subscription",
|
||||
"getSubscription": "Get Subscription",
|
||||
"cta": {
|
||||
"expiredHint": "Choose a plan and pay",
|
||||
"trialHint": "More traffic and devices",
|
||||
"activeHint": "Renewal and plan change"
|
||||
},
|
||||
"connectionInfo": "Connection Info",
|
||||
"copyLink": "Copy Link",
|
||||
"copied": "Copied!",
|
||||
|
||||
@@ -336,6 +336,11 @@
|
||||
"price": "Цена",
|
||||
"noSubscription": "У вас нет активной подписки",
|
||||
"getSubscription": "Оформить подписку",
|
||||
"cta": {
|
||||
"expiredHint": "Выберите тариф и оплатите",
|
||||
"trialHint": "Больше трафика и устройств",
|
||||
"activeHint": "Продление и смена тарифа"
|
||||
},
|
||||
"connectionInfo": "Данные для подключения",
|
||||
"copyLink": "Копировать ссылку",
|
||||
"copied": "Скопировано!",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1900
src/pages/SubscriptionPurchase.tsx
Normal file
1900
src/pages/SubscriptionPurchase.tsx
Normal file
File diff suppressed because it is too large
Load Diff
42
src/utils/subscriptionHelpers.ts
Normal file
42
src/utils/subscriptionHelpers.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { AxiosError } from 'axios';
|
||||
import i18n from '../i18n';
|
||||
|
||||
export type PurchaseStep = 'period' | 'traffic' | 'servers' | 'devices' | 'confirm';
|
||||
|
||||
export const getErrorMessage = (error: unknown): string => {
|
||||
if (error instanceof AxiosError) {
|
||||
const detail = error.response?.data?.detail;
|
||||
if (typeof detail === 'string') return detail;
|
||||
if (typeof detail === 'object' && detail?.message) return detail.message;
|
||||
}
|
||||
if (error instanceof Error) return error.message;
|
||||
return i18n.t('common.error');
|
||||
};
|
||||
|
||||
export const getInsufficientBalanceError = (
|
||||
error: unknown,
|
||||
): { required: number; balance: number; missingAmount?: number } | null => {
|
||||
if (error instanceof AxiosError) {
|
||||
const detail = error.response?.data?.detail;
|
||||
if (
|
||||
typeof detail === 'object' &&
|
||||
(detail?.code === 'insufficient_balance' || detail?.code === 'insufficient_funds')
|
||||
) {
|
||||
return {
|
||||
required: detail.required || detail.total_price || 0,
|
||||
balance: detail.balance || 0,
|
||||
missingAmount: detail.missing_amount || detail.missingAmount || 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getFlagEmoji = (countryCode: string): string => {
|
||||
if (!countryCode || countryCode.length !== 2) return '';
|
||||
const codePoints = countryCode
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map((char) => 127397 + char.charCodeAt(0));
|
||||
return String.fromCodePoint(...codePoints);
|
||||
};
|
||||
Reference in New Issue
Block a user