mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
refactor(subscription): extract DeleteSubscriptionSheet from god page
Move the delete-subscription expand/confirm block into src/components/subscription/sheets/DeleteSubscriptionSheet.tsx. The sheet itself decides between the Telegram destructive native dialog and the inline web confirm panel, so the parent no longer needs the platform discriminator. It owns deleteLoading and the API call; the parent passes onDeleted (navigate + invalidate) and the textSecondary glass color token used in the warning copy. Subscription.tsx: 1828 → 1750 lines. Drops now-unused imports (usePlatform; deleteLoading state).
This commit is contained in:
127
src/components/subscription/sheets/DeleteSubscriptionSheet.tsx
Normal file
127
src/components/subscription/sheets/DeleteSubscriptionSheet.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { subscriptionApi } from '../../../api/subscription';
|
||||||
|
import { usePlatform } from '../../../platform';
|
||||||
|
import { useDestructiveConfirm } from '../../../platform/hooks/useNativeDialog';
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
// Delete-subscription sheet. Telegram path goes through the native
|
||||||
|
// destructive dialog and skips the inline expand; the web path expands
|
||||||
|
// an inline confirm panel with two buttons.
|
||||||
|
//
|
||||||
|
// Self-owns deleteLoading; parent only supplies the id, theme colour
|
||||||
|
// hints, open-state, and the onDeleted callback (used for navigate +
|
||||||
|
// invalidate after the API resolves).
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface DeleteSubscriptionSheetProps {
|
||||||
|
subscriptionId: number;
|
||||||
|
open: boolean;
|
||||||
|
onOpen: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
onDeleted: () => void;
|
||||||
|
/** color/background tokens already resolved from glassTheme */
|
||||||
|
textSecondary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteSubscriptionSheet({
|
||||||
|
subscriptionId,
|
||||||
|
open,
|
||||||
|
onOpen,
|
||||||
|
onClose,
|
||||||
|
onDeleted,
|
||||||
|
textSecondary,
|
||||||
|
}: DeleteSubscriptionSheetProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { platform } = usePlatform();
|
||||||
|
const destructiveConfirm = useDestructiveConfirm();
|
||||||
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
const performDelete = async () => {
|
||||||
|
setDeleteLoading(true);
|
||||||
|
try {
|
||||||
|
await subscriptionApi.deleteSubscription(subscriptionId);
|
||||||
|
onDeleted();
|
||||||
|
} catch {
|
||||||
|
setDeleteLoading(false);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTriggerClick = async () => {
|
||||||
|
if (platform === 'telegram') {
|
||||||
|
const confirmed = await destructiveConfirm(
|
||||||
|
t(
|
||||||
|
'subscription.deleteWarning',
|
||||||
|
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны.',
|
||||||
|
),
|
||||||
|
t('subscription.confirmDelete', 'Да, удалить'),
|
||||||
|
t('subscription.deleteTitle', 'Удалить подписку?'),
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
await performDelete();
|
||||||
|
} else {
|
||||||
|
onOpen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleTriggerClick}
|
||||||
|
disabled={deleteLoading}
|
||||||
|
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-error-400/20 bg-error-400/5 p-3.5 text-sm font-medium text-error-400 transition-colors hover:bg-error-400/10 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{t('subscription.delete', 'Удалить подписку')}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded-2xl border border-error-400/20 p-4"
|
||||||
|
style={{ background: 'rgba(255,59,92,0.04)' }}
|
||||||
|
>
|
||||||
|
<div className="mb-3 text-sm font-semibold text-error-400">
|
||||||
|
{t('subscription.deleteTitle', 'Удалить подписку?')}
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 text-xs" style={{ color: textSecondary }}>
|
||||||
|
{t(
|
||||||
|
'subscription.deleteWarning',
|
||||||
|
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны. Это действие нельзя отменить.',
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={performDelete}
|
||||||
|
disabled={deleteLoading}
|
||||||
|
className="flex-1 rounded-xl bg-error-500 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-error-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{deleteLoading
|
||||||
|
? t('common.processing', 'Удаление...')
|
||||||
|
: t('subscription.confirmDelete', 'Да, удалить')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex-1 rounded-xl border border-dark-700 py-2.5 text-sm font-medium transition-colors hover:bg-dark-700"
|
||||||
|
style={{ color: textSecondary }}
|
||||||
|
>
|
||||||
|
{t('common.cancel', 'Отмена')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import { subscriptionApi } from '../api/subscription';
|
|||||||
import { DEVICE_ALIAS_MAX_LENGTH } from '../constants/devices';
|
import { DEVICE_ALIAS_MAX_LENGTH } from '../constants/devices';
|
||||||
import { WebBackButton } from '../components/WebBackButton';
|
import { WebBackButton } from '../components/WebBackButton';
|
||||||
import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
|
import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
|
||||||
import { usePlatform } from '../platform';
|
|
||||||
import TrafficProgressBar from '../components/dashboard/TrafficProgressBar';
|
import TrafficProgressBar from '../components/dashboard/TrafficProgressBar';
|
||||||
import { HoverBorderGradient } from '../components/ui/hover-border-gradient';
|
import { HoverBorderGradient } from '../components/ui/hover-border-gradient';
|
||||||
import { useTrafficZone } from '../hooks/useTrafficZone';
|
import { useTrafficZone } from '../hooks/useTrafficZone';
|
||||||
@@ -31,6 +30,7 @@ import { DeviceTopupSheet } from '../components/subscription/sheets/DeviceTopupS
|
|||||||
import { DeviceReductionSheet } from '../components/subscription/sheets/DeviceReductionSheet';
|
import { DeviceReductionSheet } from '../components/subscription/sheets/DeviceReductionSheet';
|
||||||
import { TrafficTopupSheet } from '../components/subscription/sheets/TrafficTopupSheet';
|
import { TrafficTopupSheet } from '../components/subscription/sheets/TrafficTopupSheet';
|
||||||
import { ServerManagementSheet } from '../components/subscription/sheets/ServerManagementSheet';
|
import { ServerManagementSheet } from '../components/subscription/sheets/ServerManagementSheet';
|
||||||
|
import { DeleteSubscriptionSheet } from '../components/subscription/sheets/DeleteSubscriptionSheet';
|
||||||
|
|
||||||
/** Isolated countdown so 1s interval doesn't re-render the whole page */
|
/** Isolated countdown so 1s interval doesn't re-render the whole page */
|
||||||
const CountdownTimer = memo(function CountdownTimer({
|
const CountdownTimer = memo(function CountdownTimer({
|
||||||
@@ -195,8 +195,6 @@ export default function Subscription() {
|
|||||||
const haptic = useHaptic();
|
const haptic = useHaptic();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [showDeleteSheet, setShowDeleteSheet] = useState(false);
|
const [showDeleteSheet, setShowDeleteSheet] = useState(false);
|
||||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
|
||||||
const { platform } = usePlatform();
|
|
||||||
const destructiveConfirm = useDestructiveConfirm();
|
const destructiveConfirm = useDestructiveConfirm();
|
||||||
|
|
||||||
// Helper to format price from kopeks
|
// Helper to format price from kopeks
|
||||||
@@ -1340,93 +1338,17 @@ export default function Subscription() {
|
|||||||
!subscription.is_trial &&
|
!subscription.is_trial &&
|
||||||
!subscription.is_limited && (
|
!subscription.is_limited && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{!showDeleteSheet ? (
|
<DeleteSubscriptionSheet
|
||||||
<button
|
subscriptionId={subscription.id}
|
||||||
onClick={async () => {
|
open={showDeleteSheet}
|
||||||
if (platform === 'telegram') {
|
onOpen={() => setShowDeleteSheet(true)}
|
||||||
const confirmed = await destructiveConfirm(
|
onClose={() => setShowDeleteSheet(false)}
|
||||||
t(
|
textSecondary={g.textSecondary}
|
||||||
'subscription.deleteWarning',
|
onDeleted={() => {
|
||||||
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны.',
|
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||||||
),
|
navigate('/subscriptions', { replace: true });
|
||||||
t('subscription.confirmDelete', 'Да, удалить'),
|
}}
|
||||||
t('subscription.deleteTitle', 'Удалить подписку?'),
|
/>
|
||||||
);
|
|
||||||
if (!confirmed) return;
|
|
||||||
setDeleteLoading(true);
|
|
||||||
try {
|
|
||||||
await subscriptionApi.deleteSubscription(subscription.id);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
|
||||||
navigate('/subscriptions', { replace: true });
|
|
||||||
} catch {
|
|
||||||
setDeleteLoading(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setShowDeleteSheet(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={deleteLoading}
|
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-error-400/20 bg-error-400/5 p-3.5 text-sm font-medium text-error-400 transition-colors hover:bg-error-400/10 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{t('subscription.delete', 'Удалить подписку')}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="rounded-2xl border border-error-400/20 p-4"
|
|
||||||
style={{ background: 'rgba(255,59,92,0.04)' }}
|
|
||||||
>
|
|
||||||
<div className="mb-3 text-sm font-semibold text-error-400">
|
|
||||||
{t('subscription.deleteTitle', 'Удалить подписку?')}
|
|
||||||
</div>
|
|
||||||
<div className="mb-4 text-xs" style={{ color: g.textSecondary }}>
|
|
||||||
{t(
|
|
||||||
'subscription.deleteWarning',
|
|
||||||
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны. Это действие нельзя отменить.',
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
setDeleteLoading(true);
|
|
||||||
try {
|
|
||||||
await subscriptionApi.deleteSubscription(subscription.id);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
|
||||||
navigate('/subscriptions', { replace: true });
|
|
||||||
} catch {
|
|
||||||
setDeleteLoading(false);
|
|
||||||
setShowDeleteSheet(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={deleteLoading}
|
|
||||||
className="flex-1 rounded-xl bg-error-500 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-error-600 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{deleteLoading
|
|
||||||
? t('common.processing', 'Удаление...')
|
|
||||||
: t('subscription.confirmDelete', 'Да, удалить')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowDeleteSheet(false)}
|
|
||||||
className="flex-1 rounded-xl border border-dark-700 py-2.5 text-sm font-medium transition-colors hover:bg-dark-700"
|
|
||||||
style={{ color: g.textSecondary }}
|
|
||||||
>
|
|
||||||
{t('common.cancel', 'Отмена')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user