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 */}
+
+ >
);
}