@startsimpli/ui 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/__mocks__/next/link.js +11 -0
- package/src/components/ActivityTimeline.tsx +173 -0
- package/src/components/LogActivityDialog.tsx +303 -0
- package/src/components/QuickLogButtons.tsx +32 -0
- package/src/components/account/__tests__/account.test.tsx +315 -0
- package/src/components/badge/StageBadge.tsx +31 -0
- package/src/components/badge/index.ts +3 -0
- package/src/components/command-palette/CommandGroup.tsx +23 -0
- package/src/components/command-palette/CommandPalette.tsx +327 -0
- package/src/components/command-palette/CommandResultItem.tsx +59 -0
- package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
- package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
- package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
- package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
- package/src/components/command-palette/command-palette-context.tsx +51 -0
- package/src/components/command-palette/index.ts +9 -0
- package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
- package/src/components/compose/__tests__/compose.test.tsx +656 -0
- package/src/components/compose/compose-header.tsx +72 -0
- package/src/components/compose/compose-loading.tsx +13 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/save-status-indicator.tsx +57 -0
- package/src/components/compose/send-confirmation-dialog.tsx +87 -0
- package/src/components/compose/subject-input.tsx +25 -0
- package/src/components/compose/useAutoSave.ts +93 -0
- package/src/components/dashboard/DashboardGrid.tsx +32 -0
- package/src/components/dashboard/DashboardSection.tsx +32 -0
- package/src/components/dashboard/MetricCard.tsx +129 -0
- package/src/components/dashboard/PeriodSelector.tsx +55 -0
- package/src/components/dashboard/PipelineFunnel.tsx +126 -0
- package/src/components/dashboard/SparklineTrend.tsx +102 -0
- package/src/components/dashboard/TopCampaigns.tsx +132 -0
- package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
- package/src/components/dashboard/index.ts +20 -0
- package/src/components/dialog/ConfirmDialog.tsx +72 -0
- package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
- package/src/components/dialog/index.ts +3 -0
- package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -0
- package/src/components/email-dialogs/index.ts +14 -0
- package/src/components/email-dialogs/merge-fields.tsx +196 -0
- package/src/components/email-dialogs/preview-dialog.tsx +194 -0
- package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
- package/src/components/email-dialogs/template-picker.tsx +225 -0
- package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
- package/src/components/email-editor/BlockRenderer.tsx +120 -0
- package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
- package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
- package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -0
- package/src/components/email-editor/add-block-menu.tsx +151 -0
- package/src/components/email-editor/block-toolbar.tsx +73 -0
- package/src/components/email-editor/blocks/__tests__/blocks.test.tsx +818 -0
- package/src/components/email-editor/blocks/button-block.tsx +44 -0
- package/src/components/email-editor/blocks/divider-block.tsx +43 -0
- package/src/components/email-editor/blocks/footer-block.tsx +39 -0
- package/src/components/email-editor/blocks/header-block.tsx +39 -0
- package/src/components/email-editor/blocks/image-block.tsx +61 -0
- package/src/components/email-editor/blocks/index.ts +9 -0
- package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
- package/src/components/email-editor/blocks/social-block.tsx +75 -0
- package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
- package/src/components/email-editor/blocks/text-block.tsx +75 -0
- package/src/components/email-editor/editor-sidebar.tsx +66 -0
- package/src/components/email-editor/email-editor.tsx +497 -0
- package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
- package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
- package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
- package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
- package/src/components/email-editor/index.ts +51 -0
- package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
- package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
- package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
- package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
- package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
- package/src/components/email-editor/panels/index.ts +3 -0
- package/src/components/email-editor/renderer/block-renderers.ts +209 -0
- package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
- package/src/components/email-editor/types.ts +413 -0
- package/src/components/email-editor/utils/defaults.ts +116 -0
- package/src/components/email-editor/utils/undo-redo.ts +59 -0
- package/src/components/enrichment/EnrichButton.tsx +33 -0
- package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
- package/src/components/enrichment/QualityBadge.tsx +43 -0
- package/src/components/enrichment/__tests__/enrichment.test.tsx +184 -0
- package/src/components/enrichment/index.ts +8 -0
- package/src/components/gantt/GanttBoardView.tsx +71 -0
- package/src/components/gantt/GanttChart.tsx +140 -887
- package/src/components/gantt/GanttFilterBar.tsx +100 -0
- package/src/components/gantt/GanttListView.tsx +63 -0
- package/src/components/gantt/GanttTimelineView.tsx +215 -0
- package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
- package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
- package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
- package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
- package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
- package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
- package/src/components/gantt/hooks/useGanttState.ts +644 -0
- package/src/components/gantt/index.ts +10 -0
- package/src/components/gantt/types.ts +5 -5
- package/src/components/index.ts +46 -0
- package/src/components/integrations/ConnectionStatus.tsx +77 -0
- package/src/components/integrations/IntegrationCard.tsx +92 -0
- package/src/components/integrations/__tests__/integrations.test.tsx +191 -0
- package/src/components/integrations/index.ts +5 -0
- package/src/components/kanban/KanbanBoard.tsx +103 -0
- package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
- package/src/components/kanban/index.ts +2 -0
- package/src/components/lists/CreateListDialog.tsx +158 -0
- package/src/components/lists/ListCard.tsx +77 -0
- package/src/components/lists/__tests__/lists.test.tsx +263 -0
- package/src/components/lists/index.ts +5 -0
- package/src/components/loading/__tests__/loading.test.tsx +114 -0
- package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
- package/src/components/pipeline/StageTransitionModal.tsx +146 -0
- package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -0
- package/src/components/pipeline/index.ts +2 -0
- package/src/components/settings/SettingsCard.tsx +33 -0
- package/src/components/settings/SettingsLayout.tsx +28 -0
- package/src/components/settings/SettingsNav.tsx +42 -0
- package/src/components/settings/__tests__/settings.test.tsx +181 -0
- package/src/components/settings/index.ts +6 -0
- package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { ScheduleDialog } from './schedule-dialog'
|
|
2
|
+
export { TestSendDialog } from './test-send-dialog'
|
|
3
|
+
export { PreviewDialog } from './preview-dialog'
|
|
4
|
+
export type { PreviewRecipient } from './preview-dialog'
|
|
5
|
+
export { TemplatePicker } from './template-picker'
|
|
6
|
+
export type { EmailTemplate } from './template-picker'
|
|
7
|
+
export {
|
|
8
|
+
MergeFieldsMenu,
|
|
9
|
+
MergeFieldPreview,
|
|
10
|
+
replaceMergeFields,
|
|
11
|
+
DEFAULT_MERGE_FIELDS,
|
|
12
|
+
DEFAULT_CATEGORIES,
|
|
13
|
+
} from './merge-fields'
|
|
14
|
+
export type { MergeField, MergeFieldCategory } from './merge-fields'
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Button } from '../ui/button'
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuLabel,
|
|
9
|
+
DropdownMenuSeparator,
|
|
10
|
+
DropdownMenuTrigger,
|
|
11
|
+
} from '../ui/dropdown-menu'
|
|
12
|
+
import { Braces } from 'lucide-react'
|
|
13
|
+
|
|
14
|
+
export interface MergeField {
|
|
15
|
+
key: string
|
|
16
|
+
label: string
|
|
17
|
+
example: string
|
|
18
|
+
category: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MergeFieldCategory {
|
|
22
|
+
name: string
|
|
23
|
+
label: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Default merge field categories for email composition */
|
|
27
|
+
export const DEFAULT_CATEGORIES: MergeFieldCategory[] = [
|
|
28
|
+
{ name: 'recipient', label: 'Recipient' },
|
|
29
|
+
{ name: 'organization', label: 'Organization' },
|
|
30
|
+
{ name: 'sender', label: 'Sender' },
|
|
31
|
+
{ name: 'date', label: 'Date' },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
/** Default merge fields for email composition */
|
|
35
|
+
export const DEFAULT_MERGE_FIELDS: MergeField[] = [
|
|
36
|
+
// Recipient fields
|
|
37
|
+
{ key: '{{recipient.firstName}}', label: 'First Name', example: 'John', category: 'recipient' },
|
|
38
|
+
{ key: '{{recipient.lastName}}', label: 'Last Name', example: 'Smith', category: 'recipient' },
|
|
39
|
+
{ key: '{{recipient.fullName}}', label: 'Full Name', example: 'John Smith', category: 'recipient' },
|
|
40
|
+
{ key: '{{recipient.email}}', label: 'Email', example: 'john@example.com', category: 'recipient' },
|
|
41
|
+
{ key: '{{recipient.title}}', label: 'Title', example: 'Partner', category: 'recipient' },
|
|
42
|
+
|
|
43
|
+
// Organization fields
|
|
44
|
+
{ key: '{{organization.name}}', label: 'Organization', example: 'Acme Corp', category: 'organization' },
|
|
45
|
+
{ key: '{{organization.type}}', label: 'Type', example: 'Enterprise', category: 'organization' },
|
|
46
|
+
|
|
47
|
+
// Sender fields
|
|
48
|
+
{ key: '{{sender.name}}', label: 'Your Company', example: 'My Company', category: 'sender' },
|
|
49
|
+
{ key: '{{sender.contactName}}', label: 'Your Name', example: 'Jane Doe', category: 'sender' },
|
|
50
|
+
|
|
51
|
+
// Date fields
|
|
52
|
+
{ key: '{{date.today}}', label: 'Today', example: 'December 8, 2024', category: 'date' },
|
|
53
|
+
{ key: '{{date.month}}', label: 'Current Month', example: 'December', category: 'date' },
|
|
54
|
+
{ key: '{{date.quarter}}', label: 'Current Quarter', example: 'Q4 2024', category: 'date' },
|
|
55
|
+
{ key: '{{date.year}}', label: 'Current Year', example: '2024', category: 'date' },
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
interface MergeFieldsMenuProps {
|
|
59
|
+
onInsert: (field: string) => void
|
|
60
|
+
variant?: 'default' | 'compact'
|
|
61
|
+
/** Custom merge fields to display. Defaults to DEFAULT_MERGE_FIELDS. */
|
|
62
|
+
fields?: MergeField[]
|
|
63
|
+
/** Custom categories for grouping. Defaults to DEFAULT_CATEGORIES. */
|
|
64
|
+
categories?: MergeFieldCategory[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function MergeFieldsMenu({
|
|
68
|
+
onInsert,
|
|
69
|
+
variant = 'default',
|
|
70
|
+
fields = DEFAULT_MERGE_FIELDS,
|
|
71
|
+
categories = DEFAULT_CATEGORIES,
|
|
72
|
+
}: MergeFieldsMenuProps) {
|
|
73
|
+
const fieldsByCategory = categories.map((cat) => ({
|
|
74
|
+
...cat,
|
|
75
|
+
fields: fields.filter((f) => f.category === cat.name),
|
|
76
|
+
}))
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<DropdownMenu>
|
|
80
|
+
<DropdownMenuTrigger asChild>
|
|
81
|
+
{variant === 'compact' ? (
|
|
82
|
+
<Button variant="ghost" size="sm" className="h-8 gap-1">
|
|
83
|
+
<Braces className="h-4 w-4" />
|
|
84
|
+
<span className="text-xs">Merge</span>
|
|
85
|
+
</Button>
|
|
86
|
+
) : (
|
|
87
|
+
<Button variant="outline" size="sm">
|
|
88
|
+
<Braces className="mr-2 h-4 w-4" />
|
|
89
|
+
Insert Merge Field
|
|
90
|
+
</Button>
|
|
91
|
+
)}
|
|
92
|
+
</DropdownMenuTrigger>
|
|
93
|
+
<DropdownMenuContent align="start" className="w-64">
|
|
94
|
+
{fieldsByCategory.map((cat, idx) => (
|
|
95
|
+
<div key={cat.name}>
|
|
96
|
+
{idx > 0 && <DropdownMenuSeparator />}
|
|
97
|
+
<DropdownMenuLabel>{cat.label}</DropdownMenuLabel>
|
|
98
|
+
{cat.fields.map((field) => (
|
|
99
|
+
<MergeFieldItem key={field.key} field={field} onInsert={onInsert} />
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
))}
|
|
103
|
+
</DropdownMenuContent>
|
|
104
|
+
</DropdownMenu>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function MergeFieldItem({ field, onInsert }: { field: MergeField; onInsert: (field: string) => void }) {
|
|
109
|
+
return (
|
|
110
|
+
<DropdownMenuItem onClick={() => onInsert(field.key)} className="flex justify-between">
|
|
111
|
+
<span>{field.label}</span>
|
|
112
|
+
<span className="text-xs text-muted-foreground">{field.example}</span>
|
|
113
|
+
</DropdownMenuItem>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Preview component to show merge fields as visual tokens
|
|
118
|
+
interface MergeFieldPreviewProps {
|
|
119
|
+
content: string
|
|
120
|
+
sampleData?: Record<string, string>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Replace merge field tokens in content with sample data for previewing */
|
|
124
|
+
export function MergeFieldPreview({ content, sampleData }: MergeFieldPreviewProps) {
|
|
125
|
+
const defaultSampleData: Record<string, string> = {
|
|
126
|
+
'{{recipient.firstName}}': 'John',
|
|
127
|
+
'{{recipient.lastName}}': 'Smith',
|
|
128
|
+
'{{recipient.fullName}}': 'John Smith',
|
|
129
|
+
'{{recipient.email}}': 'john@example.com',
|
|
130
|
+
'{{recipient.title}}': 'Partner',
|
|
131
|
+
'{{organization.name}}': 'Acme Corp',
|
|
132
|
+
'{{organization.type}}': 'Enterprise',
|
|
133
|
+
'{{sender.name}}': 'My Company',
|
|
134
|
+
'{{sender.contactName}}': 'Jane Doe',
|
|
135
|
+
'{{date.today}}': new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }),
|
|
136
|
+
'{{date.month}}': new Date().toLocaleDateString('en-US', { month: 'long' }),
|
|
137
|
+
'{{date.quarter}}': `Q${Math.ceil((new Date().getMonth() + 1) / 3)} ${new Date().getFullYear()}`,
|
|
138
|
+
'{{date.year}}': new Date().getFullYear().toString(),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const data = { ...defaultSampleData, ...sampleData }
|
|
142
|
+
|
|
143
|
+
let preview = content
|
|
144
|
+
for (const [key, value] of Object.entries(data)) {
|
|
145
|
+
preview = preview.replace(new RegExp(key.replace(/[{}]/g, '\\$&'), 'g'), value)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return preview
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Replace merge fields in content with actual recipient data for sending */
|
|
152
|
+
export function replaceMergeFields(
|
|
153
|
+
content: string,
|
|
154
|
+
recipient: {
|
|
155
|
+
firstName?: string | null
|
|
156
|
+
lastName?: string | null
|
|
157
|
+
name?: string | null
|
|
158
|
+
email?: string | null
|
|
159
|
+
title?: string | null
|
|
160
|
+
},
|
|
161
|
+
organization?: {
|
|
162
|
+
name?: string | null
|
|
163
|
+
type?: string | null
|
|
164
|
+
},
|
|
165
|
+
sender?: {
|
|
166
|
+
name?: string | null
|
|
167
|
+
contactName?: string | null
|
|
168
|
+
}
|
|
169
|
+
): string {
|
|
170
|
+
const firstName = recipient.firstName || recipient.name?.split(' ')[0] || ''
|
|
171
|
+
const lastName = recipient.lastName || recipient.name?.split(' ').slice(1).join(' ') || ''
|
|
172
|
+
const fullName = recipient.name || `${firstName} ${lastName}`.trim()
|
|
173
|
+
|
|
174
|
+
const replacements: Record<string, string> = {
|
|
175
|
+
'{{recipient.firstName}}': firstName,
|
|
176
|
+
'{{recipient.lastName}}': lastName,
|
|
177
|
+
'{{recipient.fullName}}': fullName,
|
|
178
|
+
'{{recipient.email}}': recipient.email || '',
|
|
179
|
+
'{{recipient.title}}': recipient.title || '',
|
|
180
|
+
'{{organization.name}}': organization?.name || '',
|
|
181
|
+
'{{organization.type}}': organization?.type || '',
|
|
182
|
+
'{{sender.name}}': sender?.name || '',
|
|
183
|
+
'{{sender.contactName}}': sender?.contactName || '',
|
|
184
|
+
'{{date.today}}': new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }),
|
|
185
|
+
'{{date.month}}': new Date().toLocaleDateString('en-US', { month: 'long' }),
|
|
186
|
+
'{{date.quarter}}': `Q${Math.ceil((new Date().getMonth() + 1) / 3)} ${new Date().getFullYear()}`,
|
|
187
|
+
'{{date.year}}': new Date().getFullYear().toString(),
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let result = content
|
|
191
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
192
|
+
result = result.replace(new RegExp(key.replace(/[{}]/g, '\\$&'), 'g'), value)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return result
|
|
196
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
} from '../ui/dialog'
|
|
10
|
+
import { Button } from '../ui/button'
|
|
11
|
+
import { Label } from '../ui/label'
|
|
12
|
+
import {
|
|
13
|
+
Select,
|
|
14
|
+
SelectContent,
|
|
15
|
+
SelectItem,
|
|
16
|
+
SelectTrigger,
|
|
17
|
+
SelectValue,
|
|
18
|
+
} from '../ui/select'
|
|
19
|
+
import { Tabs, TabsList, TabsTrigger } from '../ui/tabs'
|
|
20
|
+
import { Monitor, Smartphone, Code } from 'lucide-react'
|
|
21
|
+
import { MergeFieldPreview } from './merge-fields'
|
|
22
|
+
import { cn } from '../../utils/cn'
|
|
23
|
+
|
|
24
|
+
export interface PreviewRecipient {
|
|
25
|
+
id: string
|
|
26
|
+
name: string
|
|
27
|
+
label?: string
|
|
28
|
+
/** Sample data for merge field substitution, keyed by merge field token (e.g. "{{recipient.firstName}}") */
|
|
29
|
+
mergeData?: Record<string, string>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface PreviewDialogProps {
|
|
33
|
+
open: boolean
|
|
34
|
+
onOpenChange: (open: boolean) => void
|
|
35
|
+
subject: string
|
|
36
|
+
/**
|
|
37
|
+
* Pre-rendered email body HTML. The component will wrap this in an email
|
|
38
|
+
* shell with proper styling for preview.
|
|
39
|
+
*/
|
|
40
|
+
bodyHtml: string
|
|
41
|
+
/** Recipients available for merge field preview */
|
|
42
|
+
recipients?: PreviewRecipient[]
|
|
43
|
+
/** Additional merge field sample data applied on top of recipient data */
|
|
44
|
+
sampleData?: Record<string, string>
|
|
45
|
+
/** Unsubscribe link text. Set to null to hide. */
|
|
46
|
+
unsubscribeText?: string | null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Default sample recipient for preview
|
|
50
|
+
const DEFAULT_RECIPIENT: PreviewRecipient = {
|
|
51
|
+
id: 'sample',
|
|
52
|
+
name: 'John Smith',
|
|
53
|
+
mergeData: {
|
|
54
|
+
'{{recipient.firstName}}': 'John',
|
|
55
|
+
'{{recipient.lastName}}': 'Smith',
|
|
56
|
+
'{{recipient.fullName}}': 'John Smith',
|
|
57
|
+
'{{recipient.email}}': 'john@example.com',
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function PreviewDialog({
|
|
62
|
+
open,
|
|
63
|
+
onOpenChange,
|
|
64
|
+
subject,
|
|
65
|
+
bodyHtml,
|
|
66
|
+
recipients = [],
|
|
67
|
+
sampleData,
|
|
68
|
+
unsubscribeText = 'Unsubscribe from these updates',
|
|
69
|
+
}: PreviewDialogProps) {
|
|
70
|
+
const [viewMode, setViewMode] = useState<'desktop' | 'mobile' | 'html'>('desktop')
|
|
71
|
+
const [selectedRecipientId, setSelectedRecipientId] = useState<string>('sample')
|
|
72
|
+
|
|
73
|
+
const previewRecipients = recipients.length > 0 ? recipients : [DEFAULT_RECIPIENT]
|
|
74
|
+
const selectedRecipient = previewRecipients.find(r => r.id === selectedRecipientId) || previewRecipients[0]
|
|
75
|
+
|
|
76
|
+
// Build merge data from selected recipient + any extra sample data
|
|
77
|
+
const mergeDataForPreview: Record<string, string> = {
|
|
78
|
+
...selectedRecipient.mergeData,
|
|
79
|
+
'{{date.today}}': new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }),
|
|
80
|
+
'{{date.month}}': new Date().toLocaleDateString('en-US', { month: 'long' }),
|
|
81
|
+
'{{date.quarter}}': `Q${Math.ceil((new Date().getMonth() + 1) / 3)} ${new Date().getFullYear()}`,
|
|
82
|
+
'{{date.year}}': new Date().getFullYear().toString(),
|
|
83
|
+
...sampleData,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const previewSubject = MergeFieldPreview({ content: subject, sampleData: mergeDataForPreview })
|
|
87
|
+
const previewBody = MergeFieldPreview({ content: bodyHtml, sampleData: mergeDataForPreview })
|
|
88
|
+
|
|
89
|
+
const fullEmailHtml = `
|
|
90
|
+
<!DOCTYPE html>
|
|
91
|
+
<html>
|
|
92
|
+
<head>
|
|
93
|
+
<meta charset="utf-8">
|
|
94
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
95
|
+
<title>${previewSubject}</title>
|
|
96
|
+
</head>
|
|
97
|
+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f3f4f6;">
|
|
98
|
+
<div style="max-width: 600px; margin: 0 auto; padding: 24px;">
|
|
99
|
+
<div style="background: white; border-radius: 8px; padding: 32px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
|
100
|
+
${previewBody}
|
|
101
|
+
</div>
|
|
102
|
+
${unsubscribeText ? `
|
|
103
|
+
<div style="text-align: center; padding: 24px; font-size: 12px; color: #6b7280;">
|
|
104
|
+
<a href="#" style="color: #6b7280;">${unsubscribeText}</a>
|
|
105
|
+
</div>
|
|
106
|
+
` : ''}
|
|
107
|
+
</div>
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
110
|
+
`.trim()
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
114
|
+
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
|
|
115
|
+
<DialogHeader>
|
|
116
|
+
<DialogTitle>Preview Email</DialogTitle>
|
|
117
|
+
</DialogHeader>
|
|
118
|
+
|
|
119
|
+
<div className="flex items-center justify-between gap-4 pb-4 border-b">
|
|
120
|
+
<div className="flex items-center gap-4">
|
|
121
|
+
<div className="space-y-1">
|
|
122
|
+
<Label className="text-xs text-muted-foreground">Preview as</Label>
|
|
123
|
+
<Select value={selectedRecipientId} onValueChange={setSelectedRecipientId}>
|
|
124
|
+
<SelectTrigger className="w-[200px]">
|
|
125
|
+
<SelectValue />
|
|
126
|
+
</SelectTrigger>
|
|
127
|
+
<SelectContent>
|
|
128
|
+
{previewRecipients.map(r => (
|
|
129
|
+
<SelectItem key={r.id} value={r.id}>
|
|
130
|
+
{r.label || r.name}
|
|
131
|
+
</SelectItem>
|
|
132
|
+
))}
|
|
133
|
+
</SelectContent>
|
|
134
|
+
</Select>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as typeof viewMode)}>
|
|
139
|
+
<TabsList>
|
|
140
|
+
<TabsTrigger value="desktop" className="gap-2">
|
|
141
|
+
<Monitor className="h-4 w-4" />
|
|
142
|
+
Desktop
|
|
143
|
+
</TabsTrigger>
|
|
144
|
+
<TabsTrigger value="mobile" className="gap-2">
|
|
145
|
+
<Smartphone className="h-4 w-4" />
|
|
146
|
+
Mobile
|
|
147
|
+
</TabsTrigger>
|
|
148
|
+
<TabsTrigger value="html" className="gap-2">
|
|
149
|
+
<Code className="h-4 w-4" />
|
|
150
|
+
HTML
|
|
151
|
+
</TabsTrigger>
|
|
152
|
+
</TabsList>
|
|
153
|
+
</Tabs>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div className="flex-1 overflow-auto">
|
|
157
|
+
{/* Subject line preview */}
|
|
158
|
+
<div className="p-4 bg-muted/50 rounded-lg mb-4">
|
|
159
|
+
<Label className="text-xs text-muted-foreground">Subject</Label>
|
|
160
|
+
<p className="font-medium mt-1">{previewSubject}</p>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{viewMode === 'html' ? (
|
|
164
|
+
<div className="bg-muted rounded-lg p-4 overflow-auto">
|
|
165
|
+
<pre className="text-xs whitespace-pre-wrap font-mono">{fullEmailHtml}</pre>
|
|
166
|
+
</div>
|
|
167
|
+
) : (
|
|
168
|
+
<div
|
|
169
|
+
className={cn(
|
|
170
|
+
'bg-gray-100 rounded-lg p-4 mx-auto transition-all',
|
|
171
|
+
viewMode === 'mobile' ? 'max-w-[375px]' : 'max-w-full'
|
|
172
|
+
)}
|
|
173
|
+
>
|
|
174
|
+
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
|
|
175
|
+
<iframe
|
|
176
|
+
srcDoc={fullEmailHtml}
|
|
177
|
+
className="w-full border-0"
|
|
178
|
+
style={{ height: viewMode === 'mobile' ? '500px' : '400px' }}
|
|
179
|
+
title="Email preview"
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div className="flex justify-end gap-2 pt-4 border-t">
|
|
187
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
188
|
+
Close
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
</DialogContent>
|
|
192
|
+
</Dialog>
|
|
193
|
+
)
|
|
194
|
+
}
|