email-builder-pro 1.0.0

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.
@@ -0,0 +1,496 @@
1
+ 'use client';
2
+
3
+ import { EmailComponent } from '@/types/email';
4
+ import DroppableComponent from './DroppableComponent';
5
+ import DraggableComponent from './DraggableComponent';
6
+
7
+ interface ComponentRendererProps {
8
+ component: EmailComponent;
9
+ index: number;
10
+ parentId?: string | null;
11
+ }
12
+
13
+ export default function ComponentRenderer({
14
+ component,
15
+ index,
16
+ parentId,
17
+ }: ComponentRendererProps) {
18
+ // Helper to build margin style
19
+ const getMarginStyle = (props: any) => {
20
+ if (props.marginTop !== undefined || props.marginBottom !== undefined ||
21
+ props.marginLeft !== undefined || props.marginRight !== undefined) {
22
+ return {
23
+ marginTop: props.marginTop !== undefined ? `${props.marginTop}px` : '0',
24
+ marginBottom: props.marginBottom !== undefined ? `${props.marginBottom}px` : '0',
25
+ marginLeft: props.marginLeft !== undefined ? `${props.marginLeft}px` : '0',
26
+ marginRight: props.marginRight !== undefined ? `${props.marginRight}px` : '0',
27
+ };
28
+ }
29
+ return props.margin ? { margin: `${props.margin}px` } : {};
30
+ };
31
+
32
+ // Helper to build padding style
33
+ const getPaddingStyle = (props: any) => {
34
+ if (props.paddingTop !== undefined || props.paddingBottom !== undefined ||
35
+ props.paddingLeft !== undefined || props.paddingRight !== undefined) {
36
+ return {
37
+ paddingTop: props.paddingTop !== undefined ? `${props.paddingTop}px` : props.padding ? `${props.padding}px` : '0',
38
+ paddingBottom: props.paddingBottom !== undefined ? `${props.paddingBottom}px` : props.padding ? `${props.padding}px` : '0',
39
+ paddingLeft: props.paddingLeft !== undefined ? `${props.paddingLeft}px` : props.padding ? `${props.padding}px` : '0',
40
+ paddingRight: props.paddingRight !== undefined ? `${props.paddingRight}px` : props.padding ? `${props.padding}px` : '0',
41
+ };
42
+ }
43
+ return props.padding ? { padding: `${props.padding}px` } : {};
44
+ };
45
+
46
+ const renderComponent = () => {
47
+ switch (component.type) {
48
+ case 'container':
49
+ return (
50
+ <DroppableComponent component={component} index={index}>
51
+ <div
52
+ style={{
53
+ backgroundColor: component.props.backgroundColor || '#ffffff',
54
+ ...getPaddingStyle(component.props),
55
+ ...getMarginStyle(component.props),
56
+ borderWidth: component.props.borderWidth ? `${component.props.borderWidth}px` : '0',
57
+ borderStyle: component.props.borderWidth ? 'solid' : 'none',
58
+ borderColor: component.props.borderColor || '#e0e0e0',
59
+ borderRadius: component.props.borderRadius ? `${component.props.borderRadius}px` : '0',
60
+ minHeight: '40px',
61
+ }}
62
+ >
63
+ {component.children && component.children.length > 0 ? (
64
+ component.children.map((child, i) => (
65
+ <DraggableComponent
66
+ key={child.id}
67
+ component={child}
68
+ index={i}
69
+ parentId={component.id}
70
+ />
71
+ ))
72
+ ) : (
73
+ <div className="text-gray-300 text-xs text-center py-2">
74
+ Drop components here
75
+ </div>
76
+ )}
77
+ </div>
78
+ </DroppableComponent>
79
+ );
80
+
81
+ case 'text':
82
+ return (
83
+ <p
84
+ style={{
85
+ fontSize: `${component.props.fontSize || 16}px`,
86
+ color: component.props.color || '#000000',
87
+ textAlign: component.props.textAlign || 'left',
88
+ fontWeight: component.props.fontWeight || 'normal',
89
+ fontFamily: component.props.fontFamily || 'Arial',
90
+ lineHeight: component.props.lineHeight || 1.5,
91
+ backgroundColor: component.props.backgroundColor || 'transparent',
92
+ ...getMarginStyle(component.props),
93
+ }}
94
+ >
95
+ {component.props.text || ''}
96
+ </p>
97
+ );
98
+
99
+ case 'heading':
100
+ const HeadingTag = `h${component.props.level || 1}` as keyof JSX.IntrinsicElements;
101
+ return (
102
+ <HeadingTag
103
+ style={{
104
+ fontSize: `${component.props.fontSize || 24}px`,
105
+ color: component.props.color || '#000000',
106
+ textAlign: component.props.textAlign || 'left',
107
+ fontWeight: component.props.fontWeight || 'bold',
108
+ ...getMarginStyle(component.props),
109
+ }}
110
+ >
111
+ {component.props.text || ''}
112
+ </HeadingTag>
113
+ );
114
+
115
+ case 'button':
116
+ return (
117
+ <a
118
+ href={component.props.href || '#'}
119
+ style={{
120
+ display: 'inline-block',
121
+ backgroundColor: component.props.backgroundColor || '#007bff',
122
+ color: component.props.color || '#ffffff',
123
+ padding: `${component.props.paddingY || component.props.padding || 12}px ${component.props.paddingX || (component.props.padding || 12) * 2}px`,
124
+ borderRadius: `${component.props.borderRadius || 4}px`,
125
+ textDecoration: 'none',
126
+ textAlign: 'center',
127
+ fontSize: `${component.props.fontSize || 14}px`,
128
+ fontWeight: component.props.fontWeight || '600',
129
+ borderWidth: component.props.borderWidth ? `${component.props.borderWidth}px` : '0',
130
+ borderStyle: component.props.borderWidth ? 'solid' : 'none',
131
+ borderColor: component.props.borderColor || '#000000',
132
+ ...getMarginStyle(component.props),
133
+ }}
134
+ >
135
+ {component.props.text || 'Button'}
136
+ </a>
137
+ );
138
+
139
+ case 'image':
140
+ return (
141
+ // eslint-disable-next-line @next/next/no-img-element
142
+ <img
143
+ src={component.props.src || ''}
144
+ alt={component.props.alt || ''}
145
+ style={{
146
+ width: component.props.width || '100%',
147
+ height: component.props.height || 'auto',
148
+ display: 'block',
149
+ marginLeft: component.props.align === 'center' ? 'auto' : component.props.align === 'right' ? 'auto' : '0',
150
+ marginRight: component.props.align === 'center' ? 'auto' : component.props.align === 'right' ? '0' : 'auto',
151
+ ...getMarginStyle(component.props),
152
+ }}
153
+ />
154
+ );
155
+
156
+ case 'divider':
157
+ return (
158
+ <hr
159
+ style={{
160
+ border: 'none',
161
+ borderTop: `${component.props.height || 1}px solid ${component.props.color || '#e0e0e0'}`,
162
+ ...getMarginStyle(component.props),
163
+ }}
164
+ />
165
+ );
166
+
167
+ case 'spacer':
168
+ return (
169
+ <div
170
+ style={{
171
+ height: `${component.props.height || 20}px`,
172
+ }}
173
+ />
174
+ );
175
+
176
+ case 'row':
177
+ return (
178
+ <DroppableComponent component={component} index={index}>
179
+ <div
180
+ style={{
181
+ display: 'flex',
182
+ gap: `${component.props.gap || 10}px`,
183
+ justifyContent: component.props.justifyContent || 'flex-start',
184
+ ...getMarginStyle(component.props),
185
+ minHeight: '40px',
186
+ }}
187
+ >
188
+ {component.children && component.children.length > 0 ? (
189
+ component.children.map((child, i) => (
190
+ <DraggableComponent
191
+ key={child.id}
192
+ component={child}
193
+ index={i}
194
+ parentId={component.id}
195
+ />
196
+ ))
197
+ ) : (
198
+ <div className="text-gray-300 text-xs text-center py-2 flex-1">
199
+ Drop components here
200
+ </div>
201
+ )}
202
+ </div>
203
+ </DroppableComponent>
204
+ );
205
+
206
+ case 'column':
207
+ return (
208
+ <DroppableComponent component={component} index={index}>
209
+ <div
210
+ style={{
211
+ width: component.props.width || '50%',
212
+ ...getMarginStyle(component.props),
213
+ minHeight: '40px',
214
+ }}
215
+ >
216
+ {component.children && component.children.length > 0 ? (
217
+ component.children.map((child, i) => (
218
+ <DraggableComponent
219
+ key={child.id}
220
+ component={child}
221
+ index={i}
222
+ parentId={component.id}
223
+ />
224
+ ))
225
+ ) : (
226
+ <div className="text-gray-300 text-xs text-center py-2">
227
+ Drop components here
228
+ </div>
229
+ )}
230
+ </div>
231
+ </DroppableComponent>
232
+ );
233
+
234
+ case 'header':
235
+ return (
236
+ <DroppableComponent component={component} index={index}>
237
+ <div
238
+ style={{
239
+ backgroundColor: component.props.backgroundColor || '#ffffff',
240
+ ...getPaddingStyle(component.props),
241
+ ...getMarginStyle(component.props),
242
+ borderBottom: component.props.borderBottom ? `1px solid ${component.props.borderColor || '#e0e0e0'}` : 'none',
243
+ }}
244
+ >
245
+ {component.props.showLogo && component.props.logoUrl && (
246
+ // eslint-disable-next-line @next/next/no-img-element
247
+ <img
248
+ src={component.props.logoUrl}
249
+ alt={component.props.logoAlt || 'Logo'}
250
+ style={{
251
+ height: component.props.logoHeight || '40px',
252
+ marginBottom: component.props.logoMarginBottom || '0',
253
+ }}
254
+ />
255
+ )}
256
+ {component.children && component.children.length > 0 ? (
257
+ component.children.map((child, i) => (
258
+ <DraggableComponent
259
+ key={child.id}
260
+ component={child}
261
+ index={i}
262
+ parentId={component.id}
263
+ />
264
+ ))
265
+ ) : (
266
+ <div className="text-gray-300 text-xs text-center py-2">
267
+ Drop header content here
268
+ </div>
269
+ )}
270
+ </div>
271
+ </DroppableComponent>
272
+ );
273
+
274
+ case 'footer':
275
+ return (
276
+ <DroppableComponent component={component} index={index}>
277
+ <div
278
+ style={{
279
+ backgroundColor: component.props.backgroundColor || '#f5f5f5',
280
+ ...getPaddingStyle(component.props),
281
+ ...getMarginStyle(component.props),
282
+ borderTop: component.props.borderTop ? `1px solid ${component.props.borderColor || '#e0e0e0'}` : 'none',
283
+ textAlign: component.props.textAlign || 'center',
284
+ }}
285
+ >
286
+ {component.children && component.children.length > 0 ? (
287
+ component.children.map((child, i) => (
288
+ <DraggableComponent
289
+ key={child.id}
290
+ component={child}
291
+ index={i}
292
+ parentId={component.id}
293
+ />
294
+ ))
295
+ ) : (
296
+ <>
297
+ {component.props.showSocialLinks && (
298
+ <div style={{ marginBottom: '15px' }}>
299
+ {/* Social links will be rendered here if added as children */}
300
+ </div>
301
+ )}
302
+ {component.props.copyright && (
303
+ <p
304
+ style={{
305
+ fontSize: `${component.props.fontSize || 12}px`,
306
+ color: component.props.color || '#666666',
307
+ margin: '10px 0',
308
+ }}
309
+ >
310
+ {component.props.copyright}
311
+ </p>
312
+ )}
313
+ </>
314
+ )}
315
+ </div>
316
+ </DroppableComponent>
317
+ );
318
+
319
+ case 'section':
320
+ return (
321
+ <DroppableComponent component={component} index={index}>
322
+ <div
323
+ style={{
324
+ backgroundColor: component.props.backgroundColor || '#ffffff',
325
+ ...getPaddingStyle(component.props),
326
+ ...getMarginStyle(component.props),
327
+ minHeight: '40px',
328
+ }}
329
+ >
330
+ {component.children && component.children.length > 0 ? (
331
+ component.children.map((child, i) => (
332
+ <DraggableComponent
333
+ key={child.id}
334
+ component={child}
335
+ index={i}
336
+ parentId={component.id}
337
+ />
338
+ ))
339
+ ) : (
340
+ <div className="text-gray-300 text-xs text-center py-2">
341
+ Drop section content here
342
+ </div>
343
+ )}
344
+ </div>
345
+ </DroppableComponent>
346
+ );
347
+
348
+ case 'link':
349
+ return (
350
+ <a
351
+ href={component.props.href || '#'}
352
+ style={{
353
+ color: component.props.color || '#007bff',
354
+ textDecoration: component.props.underline ? 'underline' : 'none',
355
+ fontSize: `${component.props.fontSize || 16}px`,
356
+ ...getMarginStyle(component.props),
357
+ }}
358
+ >
359
+ {component.props.text || 'Link'}
360
+ </a>
361
+ );
362
+
363
+ case 'list':
364
+ const ListTag = component.props.listType === 'ordered' ? 'ol' : 'ul';
365
+ return (
366
+ <ListTag
367
+ style={{
368
+ fontSize: `${component.props.fontSize || 16}px`,
369
+ color: component.props.color || '#000000',
370
+ paddingLeft: '20px',
371
+ ...getMarginStyle(component.props),
372
+ }}
373
+ >
374
+ {(component.props.items || []).map((item: string, i: number) => (
375
+ <li key={i} style={{ marginBottom: '5px' }}>
376
+ {item}
377
+ </li>
378
+ ))}
379
+ </ListTag>
380
+ );
381
+
382
+ case 'table':
383
+ return (
384
+ <table
385
+ style={{
386
+ width: '100%',
387
+ borderCollapse: 'collapse',
388
+ ...getMarginStyle(component.props),
389
+ }}
390
+ >
391
+ {component.props.headers && (
392
+ <thead>
393
+ <tr>
394
+ {component.props.headers.map((header: string, i: number) => (
395
+ <th
396
+ key={i}
397
+ style={{
398
+ border: `${component.props.borderWidth || 1}px solid ${component.props.borderColor || '#e0e0e0'}`,
399
+ padding: `${component.props.cellPadding || 10}px`,
400
+ backgroundColor: component.props.headerBackgroundColor || '#f5f5f5',
401
+ textAlign: 'left',
402
+ fontWeight: 'bold',
403
+ }}
404
+ >
405
+ {header}
406
+ </th>
407
+ ))}
408
+ </tr>
409
+ </thead>
410
+ )}
411
+ <tbody>
412
+ {(component.props.rows || []).map((row: string[], i: number) => (
413
+ <tr key={i}>
414
+ {row.map((cell: string, j: number) => (
415
+ <td
416
+ key={j}
417
+ style={{
418
+ border: `${component.props.borderWidth || 1}px solid ${component.props.borderColor || '#e0e0e0'}`,
419
+ padding: `${component.props.cellPadding || 10}px`,
420
+ }}
421
+ >
422
+ {cell}
423
+ </td>
424
+ ))}
425
+ </tr>
426
+ ))}
427
+ </tbody>
428
+ </table>
429
+ );
430
+
431
+ case 'social-links':
432
+ const socialLinks = [];
433
+ if (component.props.facebook) socialLinks.push({ name: 'Facebook', url: component.props.facebook, icon: '📘' });
434
+ if (component.props.twitter) socialLinks.push({ name: 'Twitter', url: component.props.twitter, icon: '🐦' });
435
+ if (component.props.instagram) socialLinks.push({ name: 'Instagram', url: component.props.instagram, icon: '📷' });
436
+ if (component.props.linkedin) socialLinks.push({ name: 'LinkedIn', url: component.props.linkedin, icon: '💼' });
437
+ if (component.props.youtube) socialLinks.push({ name: 'YouTube', url: component.props.youtube, icon: '📺' });
438
+
439
+ return (
440
+ <div
441
+ style={{
442
+ display: 'flex',
443
+ gap: `${component.props.gap || 10}px`,
444
+ justifyContent: component.props.align || 'center',
445
+ ...getMarginStyle(component.props),
446
+ }}
447
+ >
448
+ {socialLinks.map((link, i) => (
449
+ <a
450
+ key={i}
451
+ href={link.url}
452
+ style={{
453
+ fontSize: `${component.props.iconSize || 24}px`,
454
+ textDecoration: 'none',
455
+ display: 'inline-block',
456
+ }}
457
+ title={link.name}
458
+ >
459
+ {link.icon}
460
+ </a>
461
+ ))}
462
+ {socialLinks.length === 0 && (
463
+ <span className="text-gray-400 text-xs">Add social links in properties</span>
464
+ )}
465
+ </div>
466
+ );
467
+
468
+ case 'quote':
469
+ return (
470
+ <blockquote
471
+ style={{
472
+ fontSize: `${component.props.fontSize || 18}px`,
473
+ color: component.props.color || '#666666',
474
+ fontStyle: 'italic',
475
+ borderLeft: component.props.borderLeft ? `4px solid ${component.props.borderColor || '#007bff'}` : 'none',
476
+ paddingLeft: component.props.borderLeft ? '20px' : '0',
477
+ margin: '20px 0',
478
+ ...getMarginStyle(component.props),
479
+ }}
480
+ >
481
+ <p style={{ margin: '0 0 10px 0' }}>{component.props.text || ''}</p>
482
+ {component.props.author && (
483
+ <cite style={{ fontSize: '14px', color: '#999' }}>
484
+ — {component.props.author}
485
+ </cite>
486
+ )}
487
+ </blockquote>
488
+ );
489
+
490
+ default:
491
+ return <div>Unknown component type</div>;
492
+ }
493
+ };
494
+
495
+ return <>{renderComponent()}</>;
496
+ }
@@ -0,0 +1,84 @@
1
+ 'use client';
2
+
3
+ import { useDrag, useDrop } from 'react-dnd';
4
+ import { EmailComponent } from '@/types/email';
5
+ import { useEmailBuilder } from '@/lib/store';
6
+ import ComponentRenderer from './ComponentRenderer';
7
+ import { Trash2, GripVertical } from 'lucide-react';
8
+
9
+ interface DraggableComponentProps {
10
+ component: EmailComponent;
11
+ index: number;
12
+ parentId?: string | null;
13
+ }
14
+
15
+ export default function DraggableComponent({
16
+ component,
17
+ index,
18
+ parentId,
19
+ }: DraggableComponentProps) {
20
+ const { selectedComponent, selectComponent, removeComponent, moveComponent } =
21
+ useEmailBuilder();
22
+
23
+ const [{ isDragging }, drag] = useDrag({
24
+ type: 'existing-component',
25
+ item: { id: component.id, index, parentId },
26
+ collect: (monitor) => ({
27
+ isDragging: monitor.isDragging(),
28
+ }),
29
+ });
30
+
31
+ const [{ isOver }, drop] = useDrop({
32
+ accept: 'existing-component',
33
+ drop: (item: { id: string; index: number; parentId?: string | null }) => {
34
+ if (item.id !== component.id && item.parentId === parentId) {
35
+ // Move component to new position
36
+ moveComponent(item.index, index);
37
+ }
38
+ },
39
+ collect: (monitor) => ({
40
+ isOver: monitor.isOver(),
41
+ }),
42
+ });
43
+
44
+ const ref = (node: HTMLDivElement | null) => {
45
+ drag(drop(node));
46
+ };
47
+
48
+ return (
49
+ <div
50
+ ref={ref as any}
51
+ className={`relative group mb-2 ${
52
+ selectedComponent === component.id
53
+ ? 'ring-2 ring-primary-500'
54
+ : ''
55
+ } ${isDragging ? 'opacity-50' : ''} ${isOver ? 'ring-2 ring-blue-400' : ''}`}
56
+ onClick={(e) => {
57
+ e.stopPropagation();
58
+ selectComponent(component.id);
59
+ }}
60
+ >
61
+ <div className="absolute -left-8 top-0 opacity-0 group-hover:opacity-100 transition-opacity">
62
+ <div className="p-1 bg-gray-100 rounded cursor-move">
63
+ <GripVertical size={14} className="text-gray-400" />
64
+ </div>
65
+ </div>
66
+ <ComponentRenderer component={component} index={index} parentId={parentId} />
67
+ {selectedComponent === component.id && (
68
+ <div className="absolute -top-8 left-0 flex gap-1">
69
+ <button
70
+ onClick={(e) => {
71
+ e.stopPropagation();
72
+ removeComponent(component.id);
73
+ selectComponent(null);
74
+ }}
75
+ className="p-1 bg-red-500 text-white rounded text-xs hover:bg-red-600"
76
+ >
77
+ <Trash2 size={12} />
78
+ </button>
79
+ </div>
80
+ )}
81
+ </div>
82
+ );
83
+ }
84
+
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+
3
+ import { useDrop } from 'react-dnd';
4
+ import { EmailComponent } from '@/types/email';
5
+ import { useEmailBuilder } from '@/lib/store';
6
+ import ComponentRenderer from './ComponentRenderer';
7
+
8
+ interface DroppableComponentProps {
9
+ component: EmailComponent;
10
+ children: React.ReactNode;
11
+ index: number;
12
+ }
13
+
14
+ export default function DroppableComponent({
15
+ component,
16
+ children,
17
+ index,
18
+ }: DroppableComponentProps) {
19
+ const { addComponent } = useEmailBuilder();
20
+
21
+ const [{ isOver }, drop] = useDrop({
22
+ accept: 'component',
23
+ drop: (item: { type: string; defaultProps: Record<string, any> }, monitor) => {
24
+ // Only handle drop if this is the target (not a child)
25
+ if (monitor.didDrop()) {
26
+ return;
27
+ }
28
+ // Generate unique ID with timestamp and random number
29
+ const newComponent: EmailComponent = {
30
+ id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
31
+ type: item.type as EmailComponent['type'],
32
+ props: item.defaultProps,
33
+ };
34
+ addComponent(newComponent, undefined, component.id);
35
+ },
36
+ collect: (monitor) => ({
37
+ isOver: monitor.isOver({ shallow: true }) && !monitor.didDrop(),
38
+ }),
39
+ });
40
+
41
+ // Only make droppable if component can have children
42
+ const canHaveChildren =
43
+ component.type === 'container' ||
44
+ component.type === 'row' ||
45
+ component.type === 'column' ||
46
+ component.type === 'header' ||
47
+ component.type === 'footer' ||
48
+ component.type === 'section';
49
+
50
+ if (!canHaveChildren) {
51
+ return <>{children}</>;
52
+ }
53
+
54
+ return (
55
+ <div
56
+ ref={drop as any}
57
+ className={`relative ${isOver ? 'ring-2 ring-primary-400 ring-dashed' : ''}`}
58
+ style={{
59
+ minHeight: isOver ? '60px' : 'auto',
60
+ transition: 'all 0.2s',
61
+ position: 'relative',
62
+ }}
63
+ >
64
+ {children}
65
+ {isOver && (
66
+ <div
67
+ className="absolute inset-0 bg-primary-50 bg-opacity-50 border-2 border-dashed border-primary-400 rounded flex items-center justify-center pointer-events-none z-10"
68
+ style={{
69
+ margin: '2px',
70
+ }}
71
+ >
72
+ <span className="text-primary-600 text-sm font-medium bg-white px-3 py-1 rounded shadow-sm">
73
+ Drop component here
74
+ </span>
75
+ </div>
76
+ )}
77
+ </div>
78
+ );
79
+ }
80
+