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,59 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import ComponentPalette from './ComponentPalette';
5
+ import Canvas from './Canvas';
6
+ import PropertiesPanel from './PropertiesPanel';
7
+ import PreviewPanel from './PreviewPanel';
8
+ import Toolbar from './Toolbar';
9
+ import TemplateManager from './TemplateManager';
10
+ import { useEmailBuilder } from '@/lib/store';
11
+
12
+ export default function EmailBuilder() {
13
+ const [showPreview, setShowPreview] = useState(false);
14
+ const [showTemplates, setShowTemplates] = useState(false);
15
+ const { components } = useEmailBuilder();
16
+
17
+ return (
18
+ <div className="h-screen flex flex-col bg-gray-50">
19
+ <Toolbar
20
+ onPreview={() => setShowPreview(!showPreview)}
21
+ onTemplates={() => setShowTemplates(!showTemplates)}
22
+ showPreview={showPreview}
23
+ />
24
+
25
+ <div className="flex-1 flex overflow-hidden">
26
+ {/* Component Palette */}
27
+ <div className="w-64 bg-white border-r border-gray-200 overflow-y-auto">
28
+ <ComponentPalette />
29
+ </div>
30
+
31
+ {/* Main Canvas */}
32
+ <div className="flex-1 flex flex-col overflow-hidden">
33
+ <Canvas />
34
+ </div>
35
+
36
+ {/* Properties Panel */}
37
+ {!showPreview && (
38
+ <div className="w-80 bg-white border-l border-gray-200 overflow-y-auto">
39
+ <PropertiesPanel />
40
+ </div>
41
+ )}
42
+
43
+ {/* Preview Panel */}
44
+ {showPreview && (
45
+ <div className="w-1/2 bg-white border-l border-gray-200 overflow-y-auto">
46
+ <PreviewPanel />
47
+ </div>
48
+ )}
49
+ </div>
50
+
51
+ {/* Template Manager Modal */}
52
+ {showTemplates && (
53
+ <TemplateManager onClose={() => setShowTemplates(false)} />
54
+ )}
55
+ </div>
56
+ );
57
+ }
58
+
59
+
@@ -0,0 +1,186 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef } from 'react';
4
+ import { Upload, X, Maximize2 } from 'lucide-react';
5
+
6
+ interface ImageUploadProps {
7
+ value: string;
8
+ onChange: (url: string) => void;
9
+ onResize?: (width: number, height: number) => void;
10
+ }
11
+
12
+ export default function ImageUpload({ value, onChange, onResize }: ImageUploadProps) {
13
+ const [isUploading, setIsUploading] = useState(false);
14
+ const [showResizer, setShowResizer] = useState(false);
15
+ const [resizeWidth, setResizeWidth] = useState(400);
16
+ const [resizeHeight, setResizeHeight] = useState(300);
17
+ const fileInputRef = useRef<HTMLInputElement>(null);
18
+ const canvasRef = useRef<HTMLCanvasElement>(null);
19
+
20
+ const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
21
+ const file = e.target.files?.[0];
22
+ if (!file) return;
23
+
24
+ if (!file.type.startsWith('image/')) {
25
+ alert('Please select an image file');
26
+ return;
27
+ }
28
+
29
+ setIsUploading(true);
30
+
31
+ try {
32
+ // Convert to base64 for preview (in production, upload to server)
33
+ const reader = new FileReader();
34
+ reader.onload = (event) => {
35
+ const base64 = event.target?.result as string;
36
+ onChange(base64);
37
+
38
+ // Get image dimensions
39
+ const img = new window.Image();
40
+ img.onload = () => {
41
+ setResizeWidth(img.width);
42
+ setResizeHeight(img.height);
43
+ };
44
+ img.src = base64;
45
+ setIsUploading(false);
46
+ };
47
+ reader.readAsDataURL(file);
48
+ } catch (error) {
49
+ console.error('Error uploading image:', error);
50
+ setIsUploading(false);
51
+ }
52
+ };
53
+
54
+ const handleResize = () => {
55
+ if (!value || !canvasRef.current) return;
56
+
57
+ const img = new window.Image();
58
+ img.onload = () => {
59
+ const canvas = canvasRef.current!;
60
+ canvas.width = resizeWidth;
61
+ canvas.height = resizeHeight;
62
+ const ctx = canvas.getContext('2d');
63
+ if (ctx) {
64
+ ctx.drawImage(img, 0, 0, resizeWidth, resizeHeight);
65
+ const resizedDataUrl = canvas.toDataURL('image/png');
66
+ onChange(resizedDataUrl);
67
+ if (onResize) {
68
+ onResize(resizeWidth, resizeHeight);
69
+ }
70
+ setShowResizer(false);
71
+ }
72
+ };
73
+ img.src = value;
74
+ };
75
+
76
+ return (
77
+ <div className="space-y-4">
78
+ <div>
79
+ <label className="block text-xs font-medium text-gray-700 mb-1">
80
+ Image
81
+ </label>
82
+ {value ? (
83
+ <div className="relative group">
84
+ {/* eslint-disable-next-line @next/next/no-img-element */}
85
+ <img
86
+ src={value}
87
+ alt="Preview"
88
+ className="w-full h-32 object-cover rounded-md border border-gray-300"
89
+ />
90
+ <div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-50 transition-all rounded-md flex items-center justify-center gap-2">
91
+ <button
92
+ onClick={() => fileInputRef.current?.click()}
93
+ className="opacity-0 group-hover:opacity-100 p-2 bg-white rounded-md hover:bg-gray-100 transition-all"
94
+ >
95
+ <Upload size={16} />
96
+ </button>
97
+ <button
98
+ onClick={() => setShowResizer(true)}
99
+ className="opacity-0 group-hover:opacity-100 p-2 bg-white rounded-md hover:bg-gray-100 transition-all"
100
+ >
101
+ <Maximize2 size={16} />
102
+ </button>
103
+ <button
104
+ onClick={() => onChange('')}
105
+ className="opacity-0 group-hover:opacity-100 p-2 bg-white rounded-md hover:bg-gray-100 transition-all"
106
+ >
107
+ <X size={16} />
108
+ </button>
109
+ </div>
110
+ </div>
111
+ ) : (
112
+ <div
113
+ onClick={() => fileInputRef.current?.click()}
114
+ className="border-2 border-dashed border-gray-300 rounded-md p-8 text-center cursor-pointer hover:border-primary-400 transition-colors"
115
+ >
116
+ <Upload className="mx-auto mb-2 text-gray-400" size={24} />
117
+ <p className="text-sm text-gray-600">
118
+ {isUploading ? 'Uploading...' : 'Click to upload image'}
119
+ </p>
120
+ </div>
121
+ )}
122
+ <input
123
+ ref={fileInputRef}
124
+ type="file"
125
+ accept="image/*"
126
+ onChange={handleFileSelect}
127
+ className="hidden"
128
+ />
129
+ </div>
130
+
131
+ {showResizer && value && (
132
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
133
+ <div className="bg-white rounded-lg p-6 w-full max-w-2xl">
134
+ <h3 className="text-lg font-semibold mb-4">Resize Image</h3>
135
+ <div className="grid grid-cols-2 gap-4 mb-4">
136
+ <div>
137
+ <label className="block text-xs font-medium text-gray-700 mb-1">
138
+ Width (px)
139
+ </label>
140
+ <input
141
+ type="number"
142
+ value={resizeWidth}
143
+ onChange={(e) => setResizeWidth(parseInt(e.target.value) || 0)}
144
+ className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
145
+ />
146
+ </div>
147
+ <div>
148
+ <label className="block text-xs font-medium text-gray-700 mb-1">
149
+ Height (px)
150
+ </label>
151
+ <input
152
+ type="number"
153
+ value={resizeHeight}
154
+ onChange={(e) => setResizeHeight(parseInt(e.target.value) || 0)}
155
+ className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
156
+ />
157
+ </div>
158
+ </div>
159
+ <div className="mb-4 border border-gray-300 rounded-md p-4 bg-gray-50">
160
+ <canvas
161
+ ref={canvasRef}
162
+ className="max-w-full h-auto mx-auto"
163
+ style={{ maxHeight: '400px' }}
164
+ />
165
+ </div>
166
+ <div className="flex justify-end gap-2">
167
+ <button
168
+ onClick={() => setShowResizer(false)}
169
+ className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-md"
170
+ >
171
+ Cancel
172
+ </button>
173
+ <button
174
+ onClick={handleResize}
175
+ className="px-4 py-2 text-sm font-medium bg-primary-500 text-white rounded-md hover:bg-primary-600"
176
+ >
177
+ Apply Resize
178
+ </button>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ )}
183
+ </div>
184
+ );
185
+ }
186
+
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+
3
+ import { ChevronUp, ChevronDown } from 'lucide-react';
4
+
5
+ interface NumberInputProps {
6
+ label: string;
7
+ value: number;
8
+ onChange: (value: number) => void;
9
+ min?: number;
10
+ max?: number;
11
+ step?: number;
12
+ unit?: string;
13
+ }
14
+
15
+ export default function NumberInput({
16
+ label,
17
+ value,
18
+ onChange,
19
+ min = 0,
20
+ max = 1000,
21
+ step = 1,
22
+ unit = '',
23
+ }: NumberInputProps) {
24
+ const handleIncrement = () => {
25
+ const newValue = Math.min(value + step, max);
26
+ onChange(newValue);
27
+ };
28
+
29
+ const handleDecrement = () => {
30
+ const newValue = Math.max(value - step, min);
31
+ onChange(newValue);
32
+ };
33
+
34
+ return (
35
+ <div className="mb-4">
36
+ <label className="block text-xs font-medium text-gray-700 mb-1">
37
+ {label} {unit && `(${unit})`}
38
+ </label>
39
+ <div className="flex items-center gap-1">
40
+ <button
41
+ type="button"
42
+ onClick={handleDecrement}
43
+ disabled={value <= min}
44
+ className="p-1.5 border border-gray-300 rounded-l-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
45
+ >
46
+ <ChevronDown size={14} />
47
+ </button>
48
+ <input
49
+ type="number"
50
+ value={value}
51
+ onChange={(e) => {
52
+ const val = parseInt(e.target.value) || 0;
53
+ onChange(Math.max(min, Math.min(val, max)));
54
+ }}
55
+ min={min}
56
+ max={max}
57
+ step={step}
58
+ className="flex-1 px-3 py-2 border-t border-b border-gray-300 text-sm text-center focus:outline-none focus:ring-2 focus:ring-primary-500"
59
+ />
60
+ <button
61
+ type="button"
62
+ onClick={handleIncrement}
63
+ disabled={value >= max}
64
+ className="p-1.5 border border-gray-300 rounded-r-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
65
+ >
66
+ <ChevronUp size={14} />
67
+ </button>
68
+ </div>
69
+ </div>
70
+ );
71
+ }
72
+
73
+
@@ -0,0 +1,125 @@
1
+ 'use client';
2
+
3
+ import { useEmailBuilder } from '@/lib/store';
4
+ import ComponentRenderer from './ComponentRenderer';
5
+ import {
6
+ Html,
7
+ Head,
8
+ Body,
9
+ Container,
10
+ Preview,
11
+ Tailwind,
12
+ } from '@react-email/components';
13
+
14
+ export default function PreviewPanel() {
15
+ const { components } = useEmailBuilder();
16
+
17
+ return (
18
+ <div className="h-full flex flex-col">
19
+ <div className="p-4 border-b border-gray-200 bg-white">
20
+ <h2 className="text-sm font-semibold text-gray-700">Live Preview</h2>
21
+ <p className="text-xs text-gray-500 mt-1">
22
+ Real-time preview of your email template
23
+ </p>
24
+ </div>
25
+
26
+ <div className="flex-1 overflow-y-auto bg-gray-100 p-8">
27
+ <div className="max-w-2xl mx-auto bg-white shadow-lg rounded-lg p-8">
28
+ {components.length === 0 ? (
29
+ <div className="text-center py-12">
30
+ <p className="text-gray-400">No components to preview</p>
31
+ </div>
32
+ ) : (
33
+ <div className="email-preview">
34
+ {components.map((component, index) => (
35
+ <ComponentRenderer
36
+ key={component.id}
37
+ component={component}
38
+ index={index}
39
+ />
40
+ ))}
41
+ </div>
42
+ )}
43
+ </div>
44
+ </div>
45
+
46
+ {/* React Email Preview */}
47
+ <div className="border-t border-gray-200 bg-white p-4">
48
+ <details className="text-sm">
49
+ <summary className="cursor-pointer font-medium text-gray-700 hover:text-gray-900">
50
+ React Email Code Preview
51
+ </summary>
52
+ <div className="mt-4 p-4 bg-gray-50 rounded-md overflow-x-auto">
53
+ <pre className="text-xs text-gray-700">
54
+ {generateReactEmailCode(components)}
55
+ </pre>
56
+ </div>
57
+ </details>
58
+ </div>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ function generateReactEmailCode(components: any[]): string {
64
+ return `import {
65
+ Body,
66
+ Container,
67
+ Head,
68
+ Html,
69
+ Preview,
70
+ Tailwind,
71
+ Text,
72
+ Heading,
73
+ Button,
74
+ Img,
75
+ Hr,
76
+ Section,
77
+ } from '@react-email/components';
78
+
79
+ export default function EmailTemplate() {
80
+ return (
81
+ <Html>
82
+ <Head />
83
+ <Preview>Email Preview</Preview>
84
+ <Tailwind>
85
+ <Body className="mx-auto my-auto bg-white px-2 font-sans">
86
+ <Container className="mx-auto my-[40px] max-w-[600px]">
87
+ ${renderComponentsToReactEmail(components, 12)}
88
+ </Container>
89
+ </Body>
90
+ </Tailwind>
91
+ </Html>
92
+ );
93
+ }`;
94
+ }
95
+
96
+ function renderComponentsToReactEmail(
97
+ components: any[],
98
+ indent: number
99
+ ): string {
100
+ const spaces = ' '.repeat(indent);
101
+ return components
102
+ .map((comp) => {
103
+ switch (comp.type) {
104
+ case 'text':
105
+ return `${spaces}<Text className="text-[${comp.props.fontSize || 16}px] text-[${comp.props.color || '#000000'}] text-${comp.props.textAlign || 'left'}">${comp.props.text || ''}</Text>`;
106
+ case 'heading':
107
+ const HeadingTag = `Heading`;
108
+ return `${spaces}<${HeadingTag} className="text-[${comp.props.fontSize || 24}px] text-[${comp.props.color || '#000000'}] text-${comp.props.textAlign || 'left'}">${comp.props.text || ''}</${HeadingTag}>`;
109
+ case 'button':
110
+ return `${spaces}<Button href="${comp.props.href || '#'}" className="bg-[${comp.props.backgroundColor || '#007bff'}] text-[${comp.props.color || '#ffffff'}] px-[${(comp.props.padding || 12) * 2}px] py-[${comp.props.padding || 12}px] rounded-[${comp.props.borderRadius || 4}px]">${comp.props.text || 'Button'}</Button>`;
111
+ case 'image':
112
+ return `${spaces}<Img src="${comp.props.src || ''}" alt="${comp.props.alt || ''}" width="${comp.props.width || '100%'}" />`;
113
+ case 'divider':
114
+ return `${spaces}<Hr className="border-[${comp.props.color || '#e0e0e0'}] border-t-[${comp.props.height || 1}px]" />`;
115
+ case 'container':
116
+ return `${spaces}<Section className="bg-[${comp.props.backgroundColor || '#ffffff'}] p-[${comp.props.padding || 20}px] m-[${comp.props.margin || 0}px]">\n${comp.children ? renderComponentsToReactEmail(comp.children, indent + 2) : ''}\n${spaces}</Section>`;
117
+ default:
118
+ return '';
119
+ }
120
+ })
121
+ .filter(Boolean)
122
+ .join('\n');
123
+ }
124
+
125
+