From 8b5d777f0a94296330227b5fab34c65c83fb3baa Mon Sep 17 00:00:00 2001 From: Fringg Date: Fri, 6 Mar 2026 07:02:21 +0300 Subject: [PATCH 01/29] =?UTF-8?q?feat:=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D1=87=D0=BD=D1=8B=D0=B5=20=D0=BB=D0=B5=D0=BD=D0=B4=D0=B8=D0=BD?= =?UTF-8?q?=D0=B3-=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=B1=D1=8B=D1=81=D1=82=D1=80=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BA=D1=83=D0=BF=D0=BA=D0=B8=20VPN-=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QuickPurchase: публичная страница покупки с выбором тарифа/периода/оплаты - PurchaseSuccess: статус покупки с поллингом и копированием ссылки - AdminLandings: управление лендингами (создание, сортировка, удаление) - AdminLandingEditor: полнофункциональный редактор лендинга - API-клиент landings.ts для всех эндпоинтов - Роутинг /buy/:slug и /buy/success/:token - Локализация ru/en/zh/fa для всех текстов лендинга - Санитизация HTML/CSS, rate limit защита, DOMPurify --- src/App.tsx | 50 + src/api/client.ts | 1 + src/api/landings.ts | 199 +++ .../dashboard/SubscriptionCardActive.tsx | 19 +- src/components/dashboard/TrialOfferCard.tsx | 5 +- src/locales/en.json | 92 +- src/locales/fa.json | 90 +- src/locales/ru.json | 92 +- src/locales/zh.json | 90 +- src/pages/AdminLandingEditor.tsx | 829 +++++++++ src/pages/AdminLandings.tsx | 457 +++++ src/pages/AdminPanel.tsx | 17 + src/pages/PurchaseSuccess.tsx | 283 +++ src/pages/QuickPurchase.tsx | 831 +++++++++ src/pages/Subscription.tsx | 1518 +++++++++-------- src/pages/SubscriptionPurchase.tsx | 6 +- 16 files changed, 3813 insertions(+), 766 deletions(-) create mode 100644 src/api/landings.ts create mode 100644 src/pages/AdminLandingEditor.tsx create mode 100644 src/pages/AdminLandings.tsx create mode 100644 src/pages/PurchaseSuccess.tsx create mode 100644 src/pages/QuickPurchase.tsx diff --git a/src/App.tsx b/src/App.tsx index 26e0dad..404dc99 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,6 +37,8 @@ const Info = lazy(() => import('./pages/Info')); const Wheel = lazy(() => import('./pages/Wheel')); const Connection = lazy(() => import('./pages/Connection')); const ConnectionQR = lazy(() => import('./pages/ConnectionQR')); +const QuickPurchase = lazy(() => import('./pages/QuickPurchase')); +const PurchaseSuccess = lazy(() => import('./pages/PurchaseSuccess')); const TopUpMethodSelect = lazy(() => import('./pages/TopUpMethodSelect')); const TopUpAmount = lazy(() => import('./pages/TopUpAmount')); const ConnectedAccounts = lazy(() => import('./pages/ConnectedAccounts')); @@ -104,6 +106,8 @@ const AdminRoleAssign = lazy(() => import('./pages/AdminRoleAssign')); const AdminPolicies = lazy(() => import('./pages/AdminPolicies')); const AdminPolicyEdit = lazy(() => import('./pages/AdminPolicyEdit')); const AdminAuditLog = lazy(() => import('./pages/AdminAuditLog')); +const AdminLandings = lazy(() => import('./pages/AdminLandings')); +const AdminLandingEditor = lazy(() => import('./pages/AdminLandingEditor')); function ProtectedRoute({ children }: { children: React.ReactNode }) { const isAuthenticated = useAuthStore((state) => state.isAuthenticated); @@ -194,6 +198,22 @@ function App() { } /> + + + + } + /> + + + + } + /> {/* Protected routes */} } /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> ; + payment_methods: LandingPaymentMethod[]; + gift_enabled: boolean; + custom_css: string | null; + meta_title: string | null; + meta_description: string | null; + display_order: number; + created_at: string | null; + updated_at: string | null; +} + +export interface LandingCreateRequest { + slug: string; + title: string; + subtitle?: string; + is_active?: boolean; + features?: LandingFeature[]; + footer_text?: string; + allowed_tariff_ids?: number[]; + allowed_periods?: Record; + payment_methods?: LandingPaymentMethod[]; + gift_enabled?: boolean; + custom_css?: string; + meta_title?: string; + meta_description?: string; +} + +export type LandingUpdateRequest = Partial; + +// ============================================================ +// Public API +// ============================================================ + +export const landingApi = { + getConfig: async (slug: string): Promise => { + const response = await apiClient.get(`/cabinet/landing/${slug}`); + return response.data; + }, + + createPurchase: async (slug: string, data: PurchaseRequest): Promise => { + const response = await apiClient.post(`/cabinet/landing/${slug}/purchase`, data); + return response.data; + }, + + getPurchaseStatus: async (token: string): Promise => { + const response = await apiClient.get(`/cabinet/landing/purchase/${token}`); + return response.data; + }, +}; + +// ============================================================ +// Admin API +// ============================================================ + +export const adminLandingsApi = { + list: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/landings'); + return response.data; + }, + + get: async (id: number): Promise => { + const response = await apiClient.get(`/cabinet/admin/landings/${id}`); + return response.data; + }, + + create: async (data: LandingCreateRequest): Promise => { + const response = await apiClient.post('/cabinet/admin/landings', data); + return response.data; + }, + + update: async (id: number, data: LandingUpdateRequest): Promise => { + const response = await apiClient.put(`/cabinet/admin/landings/${id}`, data); + return response.data; + }, + + delete: async (id: number): Promise<{ message: string }> => { + const response = await apiClient.delete(`/cabinet/admin/landings/${id}`); + return response.data; + }, + + toggle: async (id: number): Promise<{ id: number; is_active: boolean; message: string }> => { + const response = await apiClient.post(`/cabinet/admin/landings/${id}/toggle`); + return response.data; + }, + + reorder: async (landingIds: number[]): Promise => { + await apiClient.put('/cabinet/admin/landings/order', { landing_ids: landingIds }); + }, +}; diff --git a/src/components/dashboard/SubscriptionCardActive.tsx b/src/components/dashboard/SubscriptionCardActive.tsx index e290930..64b487e 100644 --- a/src/components/dashboard/SubscriptionCardActive.tsx +++ b/src/components/dashboard/SubscriptionCardActive.tsx @@ -236,15 +236,24 @@ export default function SubscriptionCardActive({ {t('dashboard.connectDevice')}
- {t('dashboard.devicesOfMax', { - used: connectedDevices, - max: subscription.device_limit, - })} + {subscription.device_limit === 0 + ? t('dashboard.devicesConnectedUnlimited', { used: connectedDevices }) + : t('dashboard.devicesOfMax', { + used: connectedDevices, + max: subscription.device_limit, + })}
{/* Device indicator */} - {subscription.device_limit <= 10 ? ( + {subscription.device_limit === 0 ? ( + + ) : subscription.device_limit <= 10 ? (