From d48d053e6e3272d488e82f0895db1b18b388a71d Mon Sep 17 00:00:00 2001 From: c0mrade Date: Tue, 26 May 2026 22:21:12 +0300 Subject: [PATCH] refactor(subscription): extract ServerManagementSheet from god page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the classic-mode manage-servers sheet (~280 lines of inline JSX, the countries useQuery, its seeding useEffect, and the update mutation) into src/components/subscription/sheets/ServerManagementSheet.tsx. Parent keeps selectedServersToUpdate so the global 'close all modals' callback can still clear it; the sheet owns the rest. The query stays gated on trial state inside the sheet. Subscription.tsx: 2117 → 1828 lines (−289). --- .../sheets/ServerManagementSheet.tsx | 328 ++++++++++++++++++ src/pages/Subscription.tsx | 315 +---------------- 2 files changed, 341 insertions(+), 302 deletions(-) create mode 100644 src/components/subscription/sheets/ServerManagementSheet.tsx diff --git a/src/components/subscription/sheets/ServerManagementSheet.tsx b/src/components/subscription/sheets/ServerManagementSheet.tsx new file mode 100644 index 0000000..8617948 --- /dev/null +++ b/src/components/subscription/sheets/ServerManagementSheet.tsx @@ -0,0 +1,328 @@ +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { subscriptionApi } from '../../../api/subscription'; +import { getErrorMessage, getFlagEmoji } from '../../../utils/subscriptionHelpers'; +import InsufficientBalancePrompt from '../../InsufficientBalancePrompt'; +import type { PurchaseOptions, Subscription } from '../../../types'; + +// ────────────────────────────────────────────────────────────────── +// Manage-servers sheet (classic-mode only — caller decides whether to +// render). Self-owns the countries query + update mutation; parent +// holds the selected-uuid set so the global "close all modals" can +// reset it. +// +// Extracted from Subscription.tsx — ~280 lines off the god page. +// ────────────────────────────────────────────────────────────────── + +export interface ServerManagementSheetProps { + open: boolean; + onOpen: () => void; + onClose: () => void; + subscription: Subscription; + subscriptionId: number | undefined; + selectedServers: string[]; + onSelectedServersChange: (uuids: string[] | ((prev: string[]) => string[])) => void; + purchaseOptions: PurchaseOptions | undefined; + isDark: boolean; +} + +export function ServerManagementSheet({ + open, + onOpen, + onClose, + subscription, + subscriptionId, + selectedServers, + onSelectedServersChange, + purchaseOptions, + isDark, +}: ServerManagementSheetProps) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const formatPrice = (kopeks: number) => { + const rubles = kopeks / 100; + return rubles % 1 === 0 ? `${rubles} ₽` : `${rubles.toFixed(2)} ₽`; + }; + + const { data: countriesData, isLoading: countriesLoading } = useQuery({ + queryKey: ['countries', subscriptionId], + queryFn: () => subscriptionApi.getCountries(subscriptionId), + enabled: open && !!subscription && !subscription.is_trial, + }); + + // Seed selected = currently connected once the data loads. + useEffect(() => { + if (countriesData && open) { + const connected = countriesData.countries.filter((c) => c.is_connected).map((c) => c.uuid); + onSelectedServersChange(connected); + } + // Intentionally narrow — the setter is stable and we don't want to + // re-seed every time the user toggles a server. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [countriesData, open]); + + const updateMutation = useMutation({ + mutationFn: (countries: string[]) => subscriptionApi.updateCountries(countries, subscriptionId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['subscription', subscriptionId] }); + queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] }); + queryClient.invalidateQueries({ queryKey: ['countries', subscriptionId] }); + onClose(); + }, + }); + + if (!open) { + return ( + + ); + } + + return ( +
+
+

+ {t('subscription.additionalOptions.manageServersTitle')} +

+ +
+ + {countriesLoading ? ( +
+
+
+ ) : countriesData && countriesData.countries.length > 0 ? ( +
+
+ {t('subscription.serverManagement.statusLegend')} +
+ + {countriesData.discount_percent > 0 && ( +
+ 🎁{' '} + {t('subscription.serverManagement.discountBanner', { + percent: countriesData.discount_percent, + })} +
+ )} + +
+ {countriesData.countries + .filter((country) => country.is_available || country.is_connected) + .map((country) => { + const isCurrentlyConnected = country.is_connected; + const isSelected = selectedServers.includes(country.uuid); + const willBeAdded = !isCurrentlyConnected && isSelected; + const willBeRemoved = isCurrentlyConnected && !isSelected; + + return ( + + ); + })} +
+ + {(() => { + const currentConnected = countriesData.countries + .filter((c) => c.is_connected) + .map((c) => c.uuid); + const added = selectedServers.filter((u) => !currentConnected.includes(u)); + const removed = currentConnected.filter((u) => !selectedServers.includes(u)); + const hasChanges = added.length > 0 || removed.length > 0; + + const addedServers = countriesData.countries.filter((c) => added.includes(c.uuid)); + const totalCost = addedServers.reduce((sum, s) => sum + s.price_kopeks, 0); + const hasEnoughBalance = + !purchaseOptions || totalCost <= purchaseOptions.balance_kopeks; + const missingAmount = purchaseOptions ? totalCost - purchaseOptions.balance_kopeks : 0; + + return hasChanges ? ( +
+ {added.length > 0 && ( +
+ + {t('subscription.serverManagement.toAdd')} + {' '} + + {addedServers.map((s) => s.name).join(', ')} + +
+ )} + {removed.length > 0 && ( +
+ + {t('subscription.serverManagement.toDisconnect')} + {' '} + + {countriesData.countries + .filter((c) => removed.includes(c.uuid)) + .map((s) => s.name) + .join(', ')} + +
+ )} + {totalCost > 0 && ( +
+
+ {t('subscription.serverManagement.paymentProrated')} +
+
+ {formatPrice(totalCost)} +
+
+ )} + + {totalCost > 0 && !hasEnoughBalance && missingAmount > 0 && ( + + )} + + +
+ ) : ( +
+ {t('subscription.serverManagement.selectServersHint')} +
+ ); + })()} + + {updateMutation.isError && ( +
+ {getErrorMessage(updateMutation.error)} +
+ )} +
+ ) : ( +
+ {t('subscription.serverManagement.noServersAvailable')} +
+ )} +
+ ); +} diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx index 29fd983..d43eb7e 100644 --- a/src/pages/Subscription.tsx +++ b/src/pages/Subscription.tsx @@ -30,6 +30,7 @@ import Twemoji from 'react-twemoji'; import { DeviceTopupSheet } from '../components/subscription/sheets/DeviceTopupSheet'; import { DeviceReductionSheet } from '../components/subscription/sheets/DeviceReductionSheet'; import { TrafficTopupSheet } from '../components/subscription/sheets/TrafficTopupSheet'; +import { ServerManagementSheet } from '../components/subscription/sheets/ServerManagementSheet'; /** Isolated countdown so 1s interval doesn't re-render the whole page */ const CountdownTimer = memo(function CountdownTimer({ @@ -369,32 +370,7 @@ export default function Subscription() { // (device reduction info + mutation moved into ) // (traffic packages + purchase moved into ) - - // Countries/servers query - const { data: countriesData, isLoading: countriesLoading } = useQuery({ - queryKey: ['countries', subscriptionId], - queryFn: () => subscriptionApi.getCountries(subscriptionId), - enabled: showServerManagement && !!subscription && !subscription.is_trial, - }); - - // Initialize selected servers when data loads - useEffect(() => { - if (countriesData && showServerManagement) { - const connected = countriesData.countries.filter((c) => c.is_connected).map((c) => c.uuid); - setSelectedServersToUpdate(connected); - } - }, [countriesData, showServerManagement]); - - // Countries update mutation - const updateCountriesMutation = useMutation({ - mutationFn: (countries: string[]) => subscriptionApi.updateCountries(countries, subscriptionId), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['subscription', subscriptionId] }); - queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] }); - queryClient.invalidateQueries({ queryKey: ['countries', subscriptionId] }); - setShowServerManagement(false); - }, - }); + // (countries query + update mutation moved into ) // Traffic refresh mutation const refreshTrafficMutation = useMutation({ @@ -1519,282 +1495,17 @@ export default function Subscription() { {/* Server Management - only in classic mode */} {!isTariffsMode && (
- {!showServerManagement ? ( - - ) : ( -
-
-

- {t('subscription.additionalOptions.manageServersTitle')} -

- -
- - {countriesLoading ? ( -
-
-
- ) : countriesData && countriesData.countries.length > 0 ? ( -
-
- {t('subscription.serverManagement.statusLegend')} -
- - {countriesData.discount_percent > 0 && ( -
- 🎁{' '} - {t('subscription.serverManagement.discountBanner', { - percent: countriesData.discount_percent, - })} -
- )} - -
- {countriesData.countries - .filter((country) => country.is_available || country.is_connected) - .map((country) => { - const isCurrentlyConnected = country.is_connected; - const isSelected = selectedServersToUpdate.includes(country.uuid); - const willBeAdded = !isCurrentlyConnected && isSelected; - const willBeRemoved = isCurrentlyConnected && !isSelected; - - return ( - - ); - })} -
- - {(() => { - const currentConnected = countriesData.countries - .filter((c) => c.is_connected) - .map((c) => c.uuid); - const added = selectedServersToUpdate.filter( - (u) => !currentConnected.includes(u), - ); - const removed = currentConnected.filter( - (u) => !selectedServersToUpdate.includes(u), - ); - const hasChanges = added.length > 0 || removed.length > 0; - - // Calculate cost for added servers - const addedServers = countriesData.countries.filter((c) => - added.includes(c.uuid), - ); - const totalCost = addedServers.reduce( - (sum, s) => sum + s.price_kopeks, - 0, - ); - const hasEnoughBalance = - !purchaseOptions || totalCost <= purchaseOptions.balance_kopeks; - const missingAmount = purchaseOptions - ? totalCost - purchaseOptions.balance_kopeks - : 0; - - return hasChanges ? ( -
- {added.length > 0 && ( -
- - {t('subscription.serverManagement.toAdd')} - {' '} - - {addedServers.map((s) => s.name).join(', ')} - -
- )} - {removed.length > 0 && ( -
- - {t('subscription.serverManagement.toDisconnect')} - {' '} - - {countriesData.countries - .filter((c) => removed.includes(c.uuid)) - .map((s) => s.name) - .join(', ')} - -
- )} - {totalCost > 0 && ( -
-
- {t('subscription.serverManagement.paymentProrated')} -
-
- {formatPrice(totalCost)} -
-
- )} - - {totalCost > 0 && !hasEnoughBalance && missingAmount > 0 && ( - - )} - - -
- ) : ( -
- {t('subscription.serverManagement.selectServersHint')} -
- ); - })()} - - {updateCountriesMutation.isError && ( -
- {getErrorMessage(updateCountriesMutation.error)} -
- )} -
- ) : ( -
- {t('subscription.serverManagement.noServersAvailable')} -
- )} -
- )} + setShowServerManagement(true)} + onClose={() => setShowServerManagement(false)} + subscription={subscription} + subscriptionId={subscriptionId} + selectedServers={selectedServersToUpdate} + onSelectedServersChange={setSelectedServersToUpdate} + purchaseOptions={purchaseOptions} + isDark={isDark} + />
)}