feat: Email preview in contact and activity feed
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {Badge, Collapsible, CollapsibleContent, CollapsibleTrigger} from '@plunk/ui';
|
||||
import {Badge, Button, Collapsible, CollapsibleContent, CollapsibleTrigger} from '@plunk/ui';
|
||||
import type {Activity} from './ActivityFeed';
|
||||
import {memo} from 'react';
|
||||
import {memo, useState} from 'react';
|
||||
import {EmailPreviewModal} from './EmailPreviewModal';
|
||||
import {
|
||||
AlertCircle,
|
||||
Calendar,
|
||||
@@ -90,6 +91,13 @@ function getUpcomingTime(date: Date): string {
|
||||
return `in ${diffInMonths} ${diffInMonths === 1 ? 'month' : 'months'}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an activity is an email activity
|
||||
*/
|
||||
function isEmailActivity(type: string): boolean {
|
||||
return ['email.sent', 'email.delivered', 'email.opened', 'email.clicked', 'email.bounced'].includes(type);
|
||||
}
|
||||
|
||||
interface ActivityItemProps {
|
||||
activity: Activity;
|
||||
isUpcoming?: boolean;
|
||||
@@ -295,6 +303,7 @@ function getActivityConfig(activity: Activity): ActivityConfig {
|
||||
}
|
||||
|
||||
export const ActivityItem = memo(function ActivityItem({activity, isUpcoming = false}: ActivityItemProps) {
|
||||
const [showPreviewModal, setShowPreviewModal] = useState(false);
|
||||
const config = getActivityConfig(activity);
|
||||
const Icon = config.icon;
|
||||
const timestamp = new Date(activity.timestamp);
|
||||
@@ -318,6 +327,17 @@ export const ActivityItem = memo(function ActivityItem({activity, isUpcoming = f
|
||||
{config.title}
|
||||
</p>
|
||||
{config.badge && <Badge variant={config.badge.variant}>{config.badge.label}</Badge>}
|
||||
{isEmailActivity(activity.type) && activity.metadata.subject && activity.metadata.body ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowPreviewModal(true)}
|
||||
className="h-6 px-2 text-xs"
|
||||
>
|
||||
<Eye className="h-3 w-3 mr-1" />
|
||||
Preview
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{config.description && <p className="text-sm text-neutral-500 line-clamp-2">{config.description}</p>}
|
||||
{activity.contactEmail && (
|
||||
@@ -357,6 +377,21 @@ export const ActivityItem = memo(function ActivityItem({activity, isUpcoming = f
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Preview Modal */}
|
||||
{showPreviewModal && activity.metadata.subject && activity.metadata.body ? (
|
||||
<EmailPreviewModal
|
||||
open={showPreviewModal}
|
||||
onOpenChange={setShowPreviewModal}
|
||||
subject={String(activity.metadata.subject)}
|
||||
body={String(activity.metadata.body)}
|
||||
from={activity.metadata.from ? String(activity.metadata.from) : undefined}
|
||||
fromName={activity.metadata.fromName ? String(activity.metadata.fromName) : undefined}
|
||||
replyTo={activity.metadata.replyTo ? String(activity.metadata.replyTo) : undefined}
|
||||
toName={activity.metadata.toName ? String(activity.metadata.toName) : undefined}
|
||||
toEmail={activity.contactEmail}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import {Button, Dialog, DialogContent, DialogHeader, DialogTitle} from '@plunk/ui';
|
||||
import {Monitor, Smartphone, Tablet} from 'lucide-react';
|
||||
import {useState} from 'react';
|
||||
|
||||
interface EmailPreviewModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
subject: string;
|
||||
body: string;
|
||||
from?: string;
|
||||
fromName?: string;
|
||||
replyTo?: string;
|
||||
toName?: string;
|
||||
toEmail?: string;
|
||||
}
|
||||
|
||||
type PreviewDevice = 'mobile' | 'tablet' | 'desktop';
|
||||
|
||||
export function EmailPreviewModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
subject,
|
||||
body,
|
||||
from,
|
||||
fromName,
|
||||
replyTo,
|
||||
toName,
|
||||
toEmail,
|
||||
}: EmailPreviewModalProps) {
|
||||
const [previewDevice, setPreviewDevice] = useState<PreviewDevice>('desktop');
|
||||
|
||||
const getPreviewContainerWidth = () => {
|
||||
switch (previewDevice) {
|
||||
case 'mobile':
|
||||
return '375px';
|
||||
case 'tablet':
|
||||
return '768px';
|
||||
case 'desktop':
|
||||
return '100%';
|
||||
}
|
||||
};
|
||||
|
||||
const getDeviceLabel = () => {
|
||||
switch (previewDevice) {
|
||||
case 'mobile':
|
||||
return '375px';
|
||||
case 'tablet':
|
||||
return '768px';
|
||||
case 'desktop':
|
||||
return '1200px';
|
||||
}
|
||||
};
|
||||
|
||||
const displayFrom = fromName ? `${fromName} <${from}>` : from;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-6xl h-[90vh] flex flex-col p-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-4 flex-shrink-0">
|
||||
<DialogTitle className="pr-8">Email Preview</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Device Selector */}
|
||||
<div className="border-t border-b border-neutral-200 bg-neutral-100 px-6 py-3 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs font-medium text-neutral-600">Preview</p>
|
||||
<span className="text-xs text-neutral-500">({getDeviceLabel()})</span>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant={previewDevice === 'mobile' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setPreviewDevice('mobile')}
|
||||
className="h-7 w-7 p-0"
|
||||
title="Mobile (375px)"
|
||||
>
|
||||
<Smartphone className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={previewDevice === 'tablet' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setPreviewDevice('tablet')}
|
||||
className="h-7 w-7 p-0"
|
||||
title="Tablet (768px)"
|
||||
>
|
||||
<Tablet className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={previewDevice === 'desktop' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setPreviewDevice('desktop')}
|
||||
className="h-7 w-7 p-0"
|
||||
title="Desktop (1200px)"
|
||||
>
|
||||
<Monitor className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview Area - Single scroll container */}
|
||||
<div className="flex-1 overflow-y-auto bg-neutral-50">
|
||||
<div className="p-6 flex justify-center items-start min-h-full">
|
||||
<div
|
||||
className="transition-all duration-300"
|
||||
style={{
|
||||
width: getPreviewContainerWidth(),
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
<div className="bg-white rounded-lg border border-neutral-300 shadow-lg overflow-hidden">
|
||||
{/* Email Header Preview */}
|
||||
<div className="bg-neutral-50 border-b border-neutral-200 p-4 space-y-2">
|
||||
{/* To Section */}
|
||||
{(toName || toEmail) && (
|
||||
<div>
|
||||
<p className="text-xs text-neutral-500 uppercase tracking-wide font-medium">To</p>
|
||||
<p className="text-sm text-neutral-900 mt-1">
|
||||
{toName && toEmail ? `${toName} <${toEmail}>` : toName || toEmail}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Subject Section */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-neutral-500 uppercase tracking-wide font-medium">Subject</p>
|
||||
<p className="text-base font-semibold text-neutral-900 mt-1">{subject}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* From and Reply-To Section */}
|
||||
{(from || replyTo) && (
|
||||
<div className="flex gap-6 pt-2 border-t border-neutral-200">
|
||||
{from && (
|
||||
<div>
|
||||
<p className="text-xs text-neutral-500">From</p>
|
||||
<p className="text-sm text-neutral-900 mt-0.5">{displayFrom}</p>
|
||||
</div>
|
||||
)}
|
||||
{replyTo && (
|
||||
<div>
|
||||
<p className="text-xs text-neutral-500">Reply-To</p>
|
||||
<p className="text-sm text-neutral-900 mt-0.5">{replyTo}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email Body */}
|
||||
<div className="bg-white">
|
||||
<iframe
|
||||
sandbox="allow-same-origin"
|
||||
srcDoc={body}
|
||||
className="w-full border-0 block"
|
||||
style={{minHeight: '500px'}}
|
||||
title="Email preview"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user