feat: add destructive color variables and enhance unsubscribe banner functionality
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
* {
|
||||
|
||||
@@ -1674,8 +1674,20 @@ export function EmailViewer({
|
||||
<EmailIdentityBadge email={email} identities={identities} />
|
||||
</div>
|
||||
{sender?.email && (
|
||||
<div className="text-sm text-muted-foreground mt-0.5 truncate">
|
||||
{sender.email}
|
||||
<div className="text-sm text-muted-foreground mt-0.5 flex items-center min-w-0">
|
||||
<span className="truncate">{sender.email}</span>
|
||||
{shouldShowUnsubBanner && listHeaders?.listUnsubscribe && (
|
||||
<UnsubscribeBanner
|
||||
listUnsubscribe={listHeaders.listUnsubscribe}
|
||||
senderEmail={email?.from?.[0]?.email || ''}
|
||||
onDismiss={() => {
|
||||
const messageId = email?.messageId || '';
|
||||
const newSet = new Set(dismissedUnsubBanners).add(messageId);
|
||||
setDismissedUnsubBanners(newSet);
|
||||
localStorage.setItem('dismissed-unsub-banners', JSON.stringify([...newSet]));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -2192,6 +2204,18 @@ export function EmailViewer({
|
||||
{sender?.email && sender?.name && (
|
||||
<>
|
||||
<span className="truncate">{sender.email}</span>
|
||||
{shouldShowUnsubBanner && listHeaders?.listUnsubscribe && (
|
||||
<UnsubscribeBanner
|
||||
listUnsubscribe={listHeaders.listUnsubscribe}
|
||||
senderEmail={email?.from?.[0]?.email || ''}
|
||||
onDismiss={() => {
|
||||
const messageId = email?.messageId || '';
|
||||
const newSet = new Set(dismissedUnsubBanners).add(messageId);
|
||||
setDismissedUnsubBanners(newSet);
|
||||
localStorage.setItem('dismissed-unsub-banners', JSON.stringify([...newSet]));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span>·</span>
|
||||
</>
|
||||
)}
|
||||
@@ -2216,9 +2240,8 @@ export function EmailViewer({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Unified Notification Banner - External Content + Unsubscribe + Calendar Invitation */}
|
||||
{/* Unified Notification Banner - External Content + Calendar Invitation */}
|
||||
{((hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow') ||
|
||||
(shouldShowUnsubBanner && listHeaders?.listUnsubscribe) ||
|
||||
hasCalendarInvitation) && (
|
||||
<div className="border-b border-border bg-muted/30 isolate">
|
||||
<div className="max-w-4xl mx-auto px-6 py-1.5">
|
||||
@@ -2252,21 +2275,7 @@ export function EmailViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Unsubscribe Controls */}
|
||||
{shouldShowUnsubBanner && listHeaders?.listUnsubscribe && (
|
||||
<div className="flex items-center md:justify-center rounded-md px-3 py-1 bg-blue-50/50 dark:bg-blue-950/20">
|
||||
<UnsubscribeBanner
|
||||
listUnsubscribe={listHeaders.listUnsubscribe}
|
||||
senderEmail={email?.from?.[0]?.email || ''}
|
||||
onDismiss={() => {
|
||||
const messageId = email?.messageId || '';
|
||||
const newSet = new Set(dismissedUnsubBanners).add(messageId);
|
||||
setDismissedUnsubBanners(newSet);
|
||||
localStorage.setItem('dismissed-unsub-banners', JSON.stringify([...newSet]));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Calendar Invitation Banner */}
|
||||
{hasCalendarInvitation && (
|
||||
|
||||
@@ -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<HTMLDivElement>(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 (
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-3.5 h-3.5 text-green-600 dark:text-green-400" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="inline-flex items-center gap-1 ml-1">
|
||||
<CheckCircle className="w-3 h-3 text-green-600 dark:text-green-400" />
|
||||
<span className="text-xs text-green-600 dark:text-green-400">
|
||||
{t(unsubMethod === 'http'
|
||||
? 'email_viewer.unsubscribe_banner.success_http'
|
||||
: 'email_viewer.unsubscribe_banner.success_mailto'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<AlertCircle className="w-3.5 h-3.5 text-red-600 dark:text-red-400" />
|
||||
<span className="text-sm text-red-600 dark:text-red-400">
|
||||
{t('email_viewer.unsubscribe_banner.error')}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1 ml-1">
|
||||
<AlertCircle className="w-3 h-3 text-red-500 dark:text-red-400" />
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="text-sm text-muted-foreground hover:text-foreground bg-transparent hover:bg-transparent transition-colors min-h-[44px] md:min-h-0"
|
||||
className="text-xs text-red-500 dark:text-red-400 hover:underline bg-transparent p-0 min-h-0"
|
||||
>
|
||||
{t('email_viewer.unsubscribe_banner.dismiss')}
|
||||
{t('email_viewer.unsubscribe_banner.error')}
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{showConfirm ? (
|
||||
<>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('email_viewer.unsubscribe_banner.confirm_title')}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleUnsubscribe}
|
||||
disabled={processing}
|
||||
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground bg-transparent hover:bg-transparent transition-colors disabled:opacity-50 disabled:cursor-not-allowed min-h-[44px] md:min-h-0"
|
||||
>
|
||||
{processing && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
|
||||
{t('email_viewer.unsubscribe_banner.confirm_button')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowConfirm(false)}
|
||||
className="text-sm text-muted-foreground hover:text-foreground bg-transparent hover:bg-transparent transition-colors min-h-[44px] md:min-h-0"
|
||||
>
|
||||
{t('email_viewer.unsubscribe_banner.cancel')}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="relative inline-flex items-center">
|
||||
<span className="text-muted-foreground/40 mx-1">·</span>
|
||||
<button
|
||||
onClick={() => setShowConfirm(true)}
|
||||
className="text-sm text-muted-foreground hover:text-foreground bg-transparent hover:bg-transparent transition-colors min-h-[44px] md:min-h-0"
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline bg-transparent p-0 min-h-0 leading-normal"
|
||||
>
|
||||
{t('email_viewer.unsubscribe_banner.button')}
|
||||
</button>
|
||||
{/* Desktop popover */}
|
||||
{showConfirm && isDesktop && (
|
||||
<div
|
||||
ref={popoverRef}
|
||||
className="absolute top-full left-0 mt-1 z-50 bg-background border border-border rounded-lg shadow-lg p-3 min-w-[220px]"
|
||||
>
|
||||
<p className="text-sm text-foreground mb-2">
|
||||
{t('email_viewer.unsubscribe_banner.confirm_title')}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleUnsubscribe}
|
||||
disabled={processing}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-md bg-destructive text-destructive-foreground hover:bg-destructive/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{processing && <Loader2 className="w-3 h-3 animate-spin" />}
|
||||
{t('email_viewer.unsubscribe_banner.confirm_button')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowConfirm(false)}
|
||||
className="text-xs px-3 py-1.5 rounded-md text-muted-foreground hover:bg-muted transition-colors"
|
||||
>
|
||||
{t('email_viewer.unsubscribe_banner.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
{/* Mobile/tablet: proper confirm dialog */}
|
||||
{!isDesktop && (
|
||||
<ConfirmDialog
|
||||
isOpen={showConfirm}
|
||||
onClose={() => 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"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user