From 73212e508d5f1d6234edb1a9219cbaf73f0dd6ad Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 4 Feb 2026 02:10:08 +0300 Subject: [PATCH] Update PromoDiscountBadge.tsx --- src/components/PromoDiscountBadge.tsx | 330 +++++++++++++++++++++----- 1 file changed, 271 insertions(+), 59 deletions(-) diff --git a/src/components/PromoDiscountBadge.tsx b/src/components/PromoDiscountBadge.tsx index 519eca9..16301d0 100644 --- a/src/components/PromoDiscountBadge.tsx +++ b/src/components/PromoDiscountBadge.tsx @@ -1,6 +1,7 @@ -import { useState, useRef, useEffect } from 'react'; -import { useQuery } from '@tanstack/react-query'; +import { useState, useRef, useEffect, useCallback } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; +import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { promoApi } from '../api/promo'; @@ -30,6 +31,32 @@ const ClockIcon = () => ( ); +const XCircleIcon = () => ( + + + +); + +const WarningIcon = () => ( + + + +); + const formatTimeLeft = (expiresAt: string, t: (key: string) => string): string => { const now = new Date(); // Ensure UTC parsing - if no timezone specified, assume UTC @@ -57,10 +84,133 @@ const formatTimeLeft = (expiresAt: string, t: (key: string) => string): string = return `${minutes}${t('promo.time.minutes')}`; }; +// ------- Confirmation Modal ------- + +interface DeactivateConfirmModalProps { + isOpen: boolean; + discountPercent: number; + isDeactivating: boolean; + onConfirm: () => void; + onCancel: () => void; +} + +function DeactivateConfirmModal({ + isOpen, + discountPercent, + isDeactivating, + onConfirm, + onCancel, +}: DeactivateConfirmModalProps) { + const { t } = useTranslation(); + + // Escape key to close + useEffect(() => { + if (!isOpen) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && !isDeactivating) { + e.preventDefault(); + onCancel(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isOpen, isDeactivating, onCancel]); + + // Scroll lock + useEffect(() => { + if (!isOpen) return; + + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = ''; + }; + }, [isOpen]); + + if (!isOpen) return null; + + const modalContent = ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
e.stopPropagation()} + > + {/* Warning header */} +
+
+ +
+

+ {t('promo.deactivate.confirmTitle')} +

+

+ {t('promo.deactivate.confirmDescription', { percent: discountPercent })} +

+
+ + {/* Warning text */} +
+
+

{t('promo.deactivate.warning')}

+
+
+ + {/* Action buttons */} +
+ + +
+
+
+ ); + + if (typeof document !== 'undefined') { + return createPortal(modalContent, document.body); + } + return modalContent; +} + +// ------- Main Component ------- + export default function PromoDiscountBadge() { const { t } = useTranslation(); const navigate = useNavigate(); + const queryClient = useQueryClient(); const [isOpen, setIsOpen] = useState(false); + const [showConfirmModal, setShowConfirmModal] = useState(false); + const [deactivateError, setDeactivateError] = useState(null); const dropdownRef = useRef(null); const { data: activeDiscount } = useQuery({ @@ -70,6 +220,35 @@ export default function PromoDiscountBadge() { refetchInterval: 60000, // Refresh every minute }); + const deactivateMutation = useMutation({ + mutationFn: promoApi.deactivateDiscount, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['active-discount'] }); + queryClient.invalidateQueries({ queryKey: ['promo-offers'] }); + queryClient.invalidateQueries({ queryKey: ['subscription'] }); + queryClient.invalidateQueries({ queryKey: ['purchase-options'] }); + setShowConfirmModal(false); + setIsOpen(false); + setDeactivateError(null); + }, + onError: (error: unknown) => { + const axiosErr = error as { response?: { data?: { detail?: string } } }; + setDeactivateError(axiosErr.response?.data?.detail || t('promo.deactivate.error')); + }, + }); + + const handleCloseConfirmModal = useCallback(() => { + if (!deactivateMutation.isPending) { + setShowConfirmModal(false); + setDeactivateError(null); + } + }, [deactivateMutation.isPending]); + + const handleConfirmDeactivate = useCallback(() => { + setDeactivateError(null); + deactivateMutation.mutate(); + }, [deactivateMutation]); + // Close dropdown on click outside useEffect(() => { function handleClickOutside(event: MouseEvent) { @@ -97,70 +276,103 @@ export default function PromoDiscountBadge() { navigate('/subscription'); }; + const handleDeactivateClick = () => { + setDeactivateError(null); + setShowConfirmModal(true); + }; + return ( -
- {/* Badge button */} - + <> +
+ {/* Badge button */} + - {/* Dropdown - mobile: fixed centered, desktop: absolute right */} - {isOpen && ( - <> - {/* Mobile backdrop */} -
setIsOpen(false)} - /> + {/* Dropdown - mobile: fixed centered, desktop: absolute right */} + {isOpen && ( + <> + {/* Mobile backdrop */} +
setIsOpen(false)} + /> -
- {/* Header */} -
-
-
- -
-
-
{t('promo.discountActive')}
-
- -{activeDiscount.discount_percent}% +
+ {/* Header */} +
+
+
+ +
+
+
{t('promo.discountActive')}
+
+ -{activeDiscount.discount_percent}% +
+ + {/* Content */} +
+

{t('promo.discountDescription')}

+ + {/* Time remaining */} + {timeLeft && ( +
+ + + {t('promo.expiresIn')}:{' '} + {timeLeft} + +
+ )} + + {/* Deactivation error */} + {deactivateError && ( +
+ {deactivateError} +
+ )} + + {/* CTA Button */} + + + {/* Deactivate Button */} + +
+ + )} +
- {/* Content */} -
-

{t('promo.discountDescription')}

- - {/* Time remaining */} - {timeLeft && ( -
- - - {t('promo.expiresIn')}:{' '} - {timeLeft} - -
- )} - - {/* CTA Button */} - -
-
- - )} -
+ {/* Deactivation Confirmation Modal */} + + ); }