From 72dffe12e53ff9d74ef43da2cf653d31e7a4df25 Mon Sep 17 00:00:00 2001
From: Dries Augustyns
Date: Mon, 29 Dec 2025 15:26:53 +0100
Subject: [PATCH] feat: Email preview in contact and activity feed
---
apps/api/src/services/ActivityService.ts | 5 +
apps/web/src/components/ActivityItem.tsx | 39 +++-
apps/web/src/components/EmailPreviewModal.tsx | 173 ++++++++++++++++++
3 files changed, 215 insertions(+), 2 deletions(-)
create mode 100644 apps/web/src/components/EmailPreviewModal.tsx
diff --git a/apps/api/src/services/ActivityService.ts b/apps/api/src/services/ActivityService.ts
index f46e47e..6e3ef61 100644
--- a/apps/api/src/services/ActivityService.ts
+++ b/apps/api/src/services/ActivityService.ts
@@ -454,6 +454,11 @@ export class ActivityService {
for (const email of emails) {
const baseMetadata = {
subject: email.subject,
+ body: email.body,
+ from: email.from,
+ fromName: email.fromName,
+ replyTo: email.replyTo,
+ toName: email.toName,
sourceType: email.sourceType,
campaignName: email.campaign?.name,
workflowName: email.workflowExecution?.workflow?.name,
diff --git a/apps/web/src/components/ActivityItem.tsx b/apps/web/src/components/ActivityItem.tsx
index cf32c14..5f934e0 100644
--- a/apps/web/src/components/ActivityItem.tsx
+++ b/apps/web/src/components/ActivityItem.tsx
@@ -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}
{config.badge && {config.badge.label}}
+ {isEmailActivity(activity.type) && activity.metadata.subject && activity.metadata.body ? (
+
+ ) : null}
{config.description && {config.description}
}
{activity.contactEmail && (
@@ -357,6 +377,21 @@ export const ActivityItem = memo(function ActivityItem({activity, isUpcoming = f
+
+ {/* Email Preview Modal */}
+ {showPreviewModal && activity.metadata.subject && activity.metadata.body ? (
+
+ ) : null}
);
});
diff --git a/apps/web/src/components/EmailPreviewModal.tsx b/apps/web/src/components/EmailPreviewModal.tsx
new file mode 100644
index 0000000..52df741
--- /dev/null
+++ b/apps/web/src/components/EmailPreviewModal.tsx
@@ -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('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 (
+
+ );
+}