diff --git a/app/globals.css b/app/globals.css index d1ede0ed..c19fec0d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -16,6 +16,8 @@ --color-muted-foreground: #64748b; --color-accent: #dbeafe; --color-accent-foreground: #1e40af; + --color-destructive: #ef4444; + --color-destructive-foreground: #ffffff; /* Settings variables */ --font-size-base: 16px; @@ -37,6 +39,8 @@ --color-muted-foreground: #a3a3a3; --color-accent: #1e3a8a; --color-accent-foreground: #dbeafe; + --color-destructive: #ef4444; + --color-destructive-foreground: #fafafa; } @theme inline { @@ -53,6 +57,8 @@ --color-muted-foreground: var(--color-muted-foreground); --color-accent: var(--color-accent); --color-accent-foreground: var(--color-accent-foreground); + --color-destructive: var(--color-destructive); + --color-destructive-foreground: var(--color-destructive-foreground); } * { diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index 0a809721..c3334949 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -1674,8 +1674,20 @@ export function EmailViewer({ {sender?.email && ( -
- {sender.email} +
+ {sender.email} + {shouldShowUnsubBanner && listHeaders?.listUnsubscribe && ( + { + const messageId = email?.messageId || ''; + const newSet = new Set(dismissedUnsubBanners).add(messageId); + setDismissedUnsubBanners(newSet); + localStorage.setItem('dismissed-unsub-banners', JSON.stringify([...newSet])); + }} + /> + )}
)}
@@ -2192,6 +2204,18 @@ export function EmailViewer({ {sender?.email && sender?.name && ( <> {sender.email} + {shouldShowUnsubBanner && listHeaders?.listUnsubscribe && ( + { + const messageId = email?.messageId || ''; + const newSet = new Set(dismissedUnsubBanners).add(messageId); + setDismissedUnsubBanners(newSet); + localStorage.setItem('dismissed-unsub-banners', JSON.stringify([...newSet])); + }} + /> + )} · )} @@ -2216,9 +2240,8 @@ export function EmailViewer({ - {/* Unified Notification Banner - External Content + Unsubscribe + Calendar Invitation */} + {/* Unified Notification Banner - External Content + Calendar Invitation */} {((hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow') || - (shouldShowUnsubBanner && listHeaders?.listUnsubscribe) || hasCalendarInvitation) && (
@@ -2252,21 +2275,7 @@ export function EmailViewer({
)} - {/* Unsubscribe Controls */} - {shouldShowUnsubBanner && listHeaders?.listUnsubscribe && ( -
- { - const messageId = email?.messageId || ''; - const newSet = new Set(dismissedUnsubBanners).add(messageId); - setDismissedUnsubBanners(newSet); - localStorage.setItem('dismissed-unsub-banners', JSON.stringify([...newSet])); - }} - /> -
- )} + {/* Calendar Invitation Banner */} {hasCalendarInvitation && ( diff --git a/components/email/unsubscribe-banner.tsx b/components/email/unsubscribe-banner.tsx index 331c47fc..1f63a0e4 100644 --- a/components/email/unsubscribe-banner.tsx +++ b/components/email/unsubscribe-banner.tsx @@ -1,9 +1,11 @@ 'use client'; -import { useState } from 'react'; +import { useState, useRef, useEffect } from 'react'; import { Loader2, CheckCircle, AlertCircle } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { isValidUnsubscribeUrl } from '@/lib/validation'; +import { ConfirmDialog } from '@/components/ui/confirm-dialog'; +import { useIsDesktop } from '@/hooks/use-media-query'; interface UnsubscribeBannerProps { listUnsubscribe: { @@ -25,12 +27,31 @@ export function UnsubscribeBanner({ const [processing, setProcessing] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(false); + const popoverRef = useRef(null); + const isDesktop = useIsDesktop(); const unsubMethod = listUnsubscribe.preferred; const unsubUrl = unsubMethod === 'http' ? listUnsubscribe.http : listUnsubscribe.mailto; + useEffect(() => { + if (!showConfirm) return; + const handleClickOutside = (e: MouseEvent) => { + if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) { + setShowConfirm(false); + } + }; + // Use setTimeout to avoid the opening click triggering immediate close + const timer = setTimeout(() => { + document.addEventListener('mousedown', handleClickOutside); + }, 0); + return () => { + clearTimeout(timer); + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showConfirm]); + if (!unsubUrl || !unsubMethod) { return null; } @@ -39,6 +60,7 @@ export function UnsubscribeBanner({ if (!isValidUnsubscribeUrl(unsubUrl)) { setError(true); setProcessing(false); + setShowConfirm(false); return; } @@ -49,6 +71,7 @@ export function UnsubscribeBanner({ window.open(unsubUrl, '_blank', 'noopener,noreferrer'); setSuccess(true); setProcessing(false); + setShowConfirm(false); setTimeout(onDismiss, 3000); } else { const link = document.createElement('a'); @@ -60,76 +83,102 @@ export function UnsubscribeBanner({ setSuccess(true); setProcessing(false); + setShowConfirm(false); setTimeout(onDismiss, 3000); } } catch (err) { console.error('Unsubscribe error:', err); setError(true); setProcessing(false); + setShowConfirm(false); } }; if (success) { return ( -
- - + + + {t(unsubMethod === 'http' ? 'email_viewer.unsubscribe_banner.success_http' : 'email_viewer.unsubscribe_banner.success_mailto' )} -
+ ); } if (error) { return ( -
- - - {t('email_viewer.unsubscribe_banner.error')} - + + -
+ ); } return ( -
- {showConfirm ? ( - <> - - {t('email_viewer.unsubscribe_banner.confirm_title')} - - - - - ) : ( + <> + + · + {/* Desktop popover */} + {showConfirm && isDesktop && ( +
+

+ {t('email_viewer.unsubscribe_banner.confirm_title')} +

+
+ + +
+
+ )} +
+ {/* Mobile/tablet: proper confirm dialog */} + {!isDesktop && ( + setShowConfirm(false)} + onConfirm={() => { + setShowConfirm(false); + handleUnsubscribe(); + }} + title={t('email_viewer.unsubscribe_banner.confirm_title')} + message={t(unsubMethod === 'http' + ? 'email_viewer.unsubscribe_banner.success_http' + : 'email_viewer.unsubscribe_banner.success_mailto' + )} + confirmText={t('email_viewer.unsubscribe_banner.confirm_button')} + cancelText={t('email_viewer.unsubscribe_banner.cancel')} + variant="destructive" + /> )} -
+ ); }