feat: add destructive color variables and enhance unsubscribe banner functionality
This commit is contained in:
@@ -16,6 +16,8 @@
|
|||||||
--color-muted-foreground: #64748b;
|
--color-muted-foreground: #64748b;
|
||||||
--color-accent: #dbeafe;
|
--color-accent: #dbeafe;
|
||||||
--color-accent-foreground: #1e40af;
|
--color-accent-foreground: #1e40af;
|
||||||
|
--color-destructive: #ef4444;
|
||||||
|
--color-destructive-foreground: #ffffff;
|
||||||
|
|
||||||
/* Settings variables */
|
/* Settings variables */
|
||||||
--font-size-base: 16px;
|
--font-size-base: 16px;
|
||||||
@@ -37,6 +39,8 @@
|
|||||||
--color-muted-foreground: #a3a3a3;
|
--color-muted-foreground: #a3a3a3;
|
||||||
--color-accent: #1e3a8a;
|
--color-accent: #1e3a8a;
|
||||||
--color-accent-foreground: #dbeafe;
|
--color-accent-foreground: #dbeafe;
|
||||||
|
--color-destructive: #ef4444;
|
||||||
|
--color-destructive-foreground: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@@ -53,6 +57,8 @@
|
|||||||
--color-muted-foreground: var(--color-muted-foreground);
|
--color-muted-foreground: var(--color-muted-foreground);
|
||||||
--color-accent: var(--color-accent);
|
--color-accent: var(--color-accent);
|
||||||
--color-accent-foreground: var(--color-accent-foreground);
|
--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} />
|
<EmailIdentityBadge email={email} identities={identities} />
|
||||||
</div>
|
</div>
|
||||||
{sender?.email && (
|
{sender?.email && (
|
||||||
<div className="text-sm text-muted-foreground mt-0.5 truncate">
|
<div className="text-sm text-muted-foreground mt-0.5 flex items-center min-w-0">
|
||||||
{sender.email}
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2192,6 +2204,18 @@ export function EmailViewer({
|
|||||||
{sender?.email && sender?.name && (
|
{sender?.email && sender?.name && (
|
||||||
<>
|
<>
|
||||||
<span className="truncate">{sender.email}</span>
|
<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>
|
<span>·</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -2216,9 +2240,8 @@ export function EmailViewer({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Unified Notification Banner - External Content + Unsubscribe + Calendar Invitation */}
|
{/* Unified Notification Banner - External Content + Calendar Invitation */}
|
||||||
{((hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow') ||
|
{((hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow') ||
|
||||||
(shouldShowUnsubBanner && listHeaders?.listUnsubscribe) ||
|
|
||||||
hasCalendarInvitation) && (
|
hasCalendarInvitation) && (
|
||||||
<div className="border-b border-border bg-muted/30 isolate">
|
<div className="border-b border-border bg-muted/30 isolate">
|
||||||
<div className="max-w-4xl mx-auto px-6 py-1.5">
|
<div className="max-w-4xl mx-auto px-6 py-1.5">
|
||||||
@@ -2252,21 +2275,7 @@ export function EmailViewer({
|
|||||||
</div>
|
</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 */}
|
{/* Calendar Invitation Banner */}
|
||||||
{hasCalendarInvitation && (
|
{hasCalendarInvitation && (
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { Loader2, CheckCircle, AlertCircle } from 'lucide-react';
|
import { Loader2, CheckCircle, AlertCircle } from 'lucide-react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { isValidUnsubscribeUrl } from '@/lib/validation';
|
import { isValidUnsubscribeUrl } from '@/lib/validation';
|
||||||
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||||
|
import { useIsDesktop } from '@/hooks/use-media-query';
|
||||||
|
|
||||||
interface UnsubscribeBannerProps {
|
interface UnsubscribeBannerProps {
|
||||||
listUnsubscribe: {
|
listUnsubscribe: {
|
||||||
@@ -25,12 +27,31 @@ export function UnsubscribeBanner({
|
|||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isDesktop = useIsDesktop();
|
||||||
|
|
||||||
const unsubMethod = listUnsubscribe.preferred;
|
const unsubMethod = listUnsubscribe.preferred;
|
||||||
const unsubUrl = unsubMethod === 'http'
|
const unsubUrl = unsubMethod === 'http'
|
||||||
? listUnsubscribe.http
|
? listUnsubscribe.http
|
||||||
: listUnsubscribe.mailto;
|
: 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) {
|
if (!unsubUrl || !unsubMethod) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -39,6 +60,7 @@ export function UnsubscribeBanner({
|
|||||||
if (!isValidUnsubscribeUrl(unsubUrl)) {
|
if (!isValidUnsubscribeUrl(unsubUrl)) {
|
||||||
setError(true);
|
setError(true);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
|
setShowConfirm(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +71,7 @@ export function UnsubscribeBanner({
|
|||||||
window.open(unsubUrl, '_blank', 'noopener,noreferrer');
|
window.open(unsubUrl, '_blank', 'noopener,noreferrer');
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
|
setShowConfirm(false);
|
||||||
setTimeout(onDismiss, 3000);
|
setTimeout(onDismiss, 3000);
|
||||||
} else {
|
} else {
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
@@ -60,76 +83,102 @@ export function UnsubscribeBanner({
|
|||||||
|
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
|
setShowConfirm(false);
|
||||||
setTimeout(onDismiss, 3000);
|
setTimeout(onDismiss, 3000);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Unsubscribe error:', err);
|
console.error('Unsubscribe error:', err);
|
||||||
setError(true);
|
setError(true);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
|
setShowConfirm(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<span className="inline-flex items-center gap-1 ml-1">
|
||||||
<CheckCircle className="w-3.5 h-3.5 text-green-600 dark:text-green-400" />
|
<CheckCircle className="w-3 h-3 text-green-600 dark:text-green-400" />
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-xs text-green-600 dark:text-green-400">
|
||||||
{t(unsubMethod === 'http'
|
{t(unsubMethod === 'http'
|
||||||
? 'email_viewer.unsubscribe_banner.success_http'
|
? 'email_viewer.unsubscribe_banner.success_http'
|
||||||
: 'email_viewer.unsubscribe_banner.success_mailto'
|
: 'email_viewer.unsubscribe_banner.success_mailto'
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<span className="inline-flex items-center gap-1 ml-1">
|
||||||
<AlertCircle className="w-3.5 h-3.5 text-red-600 dark:text-red-400" />
|
<AlertCircle className="w-3 h-3 text-red-500 dark:text-red-400" />
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{t('email_viewer.unsubscribe_banner.error')}
|
|
||||||
</span>
|
|
||||||
<button
|
<button
|
||||||
onClick={onDismiss}
|
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>
|
</button>
|
||||||
</div>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<>
|
||||||
{showConfirm ? (
|
<span className="relative inline-flex items-center">
|
||||||
<>
|
<span className="text-muted-foreground/40 mx-1">·</span>
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowConfirm(true)}
|
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')}
|
{t('email_viewer.unsubscribe_banner.button')}
|
||||||
</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