@contentgrowth/content-emailing 0.4.1 → 0.6.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.
- package/dist/TemplateManager-Db41KyPN.d.cts +77 -0
- package/dist/TemplateManager-Db41KyPN.d.ts +77 -0
- package/dist/backend/EmailService.cjs +737 -0
- package/dist/backend/EmailService.cjs.map +1 -0
- package/dist/backend/EmailService.d.cts +101 -0
- package/dist/backend/EmailService.d.ts +101 -0
- package/dist/backend/EmailService.js +703 -0
- package/dist/backend/EmailService.js.map +1 -0
- package/dist/backend/EmailingCacheDO.cjs +389 -0
- package/dist/backend/EmailingCacheDO.cjs.map +1 -0
- package/dist/backend/EmailingCacheDO.d.cts +66 -0
- package/dist/backend/EmailingCacheDO.d.ts +66 -0
- package/dist/backend/EmailingCacheDO.js +364 -0
- package/dist/backend/EmailingCacheDO.js.map +1 -0
- package/dist/backend/routes/index.cjs +1001 -0
- package/dist/backend/routes/index.cjs.map +1 -0
- package/dist/backend/routes/index.d.cts +32 -0
- package/dist/backend/routes/index.d.ts +32 -0
- package/dist/backend/routes/index.js +965 -0
- package/dist/backend/routes/index.js.map +1 -0
- package/dist/cli.cjs +53 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +53 -0
- package/dist/cli.js.map +1 -0
- package/dist/common/index.cjs +267 -0
- package/dist/common/index.cjs.map +1 -0
- package/dist/common/index.d.cts +46 -0
- package/dist/common/index.d.ts +46 -0
- package/{src/common/htmlWrapper.js → dist/common/index.js} +75 -18
- package/dist/common/index.js.map +1 -0
- package/dist/frontend/index.cjs +665 -0
- package/dist/frontend/index.cjs.map +1 -0
- package/dist/frontend/index.d.cts +32 -0
- package/dist/frontend/index.d.ts +32 -0
- package/dist/frontend/index.js +626 -0
- package/dist/frontend/index.js.map +1 -0
- package/dist/index.cjs +1842 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1793 -0
- package/dist/index.js.map +1 -0
- package/package.json +31 -13
- package/examples/.env.example +0 -16
- package/examples/README.md +0 -55
- package/examples/mocks/MockD1.js +0 -311
- package/examples/mocks/MockEmailSender.js +0 -64
- package/examples/mocks/index.js +0 -5
- package/examples/package-lock.json +0 -73
- package/examples/package.json +0 -18
- package/examples/portal/index.html +0 -919
- package/examples/server.js +0 -314
- package/release.sh +0 -56
- package/src/backend/EmailService.js +0 -537
- package/src/backend/EmailingCacheDO.js +0 -466
- package/src/backend/routes/index.js +0 -30
- package/src/backend/routes/templates.js +0 -98
- package/src/backend/routes/tracking.js +0 -215
- package/src/backend/routes.js +0 -98
- package/src/common/index.js +0 -11
- package/src/common/utils.js +0 -141
- package/src/frontend/TemplateEditor.jsx +0 -117
- package/src/frontend/TemplateManager.jsx +0 -117
- package/src/index.js +0 -24
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Email Tracking Routes Factory
|
|
3
|
-
* Creates a Hono router with tracking endpoints for the email system.
|
|
4
|
-
*/
|
|
5
|
-
import { Hono } from 'hono';
|
|
6
|
-
|
|
7
|
-
// 1x1 transparent GIF pixel
|
|
8
|
-
const TRACKING_PIXEL = new Uint8Array([
|
|
9
|
-
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00,
|
|
10
|
-
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x21,
|
|
11
|
-
0xF9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00,
|
|
12
|
-
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x01, 0x44,
|
|
13
|
-
0x00, 0x3B
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Create email tracking routes
|
|
18
|
-
* @param {Object} env - Environment bindings
|
|
19
|
-
* @param {Object} config - Configuration
|
|
20
|
-
* @param {string} [config.emailTablePrefix='system_email_'] - Prefix for D1 tables
|
|
21
|
-
* @returns {Hono} Hono router
|
|
22
|
-
*/
|
|
23
|
-
export function createTrackingRoutes(env, config = {}) {
|
|
24
|
-
const app = new Hono();
|
|
25
|
-
const db = env.DB;
|
|
26
|
-
const tablePrefix = config.emailTablePrefix || config.tableNamePrefix || 'system_email_';
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* GET /track/open/:token
|
|
30
|
-
* Track email opens via a 1x1 pixel
|
|
31
|
-
*/
|
|
32
|
-
app.get('/track/open/:token', async (c) => {
|
|
33
|
-
const token = c.req.param('token');
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const sendId = token;
|
|
37
|
-
|
|
38
|
-
// Look up the send record
|
|
39
|
-
const send = await db
|
|
40
|
-
.prepare(`SELECT * FROM ${tablePrefix}sends WHERE send_id = ?`)
|
|
41
|
-
.bind(sendId)
|
|
42
|
-
.first();
|
|
43
|
-
|
|
44
|
-
if (send) {
|
|
45
|
-
// Log the open event
|
|
46
|
-
const eventId = crypto.randomUUID();
|
|
47
|
-
const now = Math.floor(Date.now() / 1000);
|
|
48
|
-
|
|
49
|
-
await db
|
|
50
|
-
.prepare(
|
|
51
|
-
`INSERT INTO ${tablePrefix}events (
|
|
52
|
-
event_id, send_id, user_id, tenant_id, email_kind, event_type, metadata, created_at
|
|
53
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
54
|
-
)
|
|
55
|
-
.bind(
|
|
56
|
-
eventId,
|
|
57
|
-
sendId,
|
|
58
|
-
send.user_id,
|
|
59
|
-
send.tenant_id,
|
|
60
|
-
send.email_kind,
|
|
61
|
-
'opened',
|
|
62
|
-
JSON.stringify({
|
|
63
|
-
user_agent: c.req.header('user-agent'),
|
|
64
|
-
referer: c.req.header('referer'),
|
|
65
|
-
}),
|
|
66
|
-
now
|
|
67
|
-
)
|
|
68
|
-
.run();
|
|
69
|
-
}
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error('[EmailTracking] Error tracking email open:', error);
|
|
72
|
-
// Don't fail the request - just serve the pixel
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Always return the tracking pixel
|
|
76
|
-
return new Response(TRACKING_PIXEL, {
|
|
77
|
-
headers: {
|
|
78
|
-
'Content-Type': 'image/gif',
|
|
79
|
-
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
80
|
-
'Pragma': 'no-cache',
|
|
81
|
-
'Expires': '0',
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* POST /track/click/:token
|
|
88
|
-
* Track email clicks
|
|
89
|
-
*/
|
|
90
|
-
app.post('/track/click/:token', async (c) => {
|
|
91
|
-
const token = c.req.param('token');
|
|
92
|
-
|
|
93
|
-
let url;
|
|
94
|
-
try {
|
|
95
|
-
const body = await c.req.json();
|
|
96
|
-
url = body.url;
|
|
97
|
-
} catch {
|
|
98
|
-
return c.json({ success: false, error: 'Invalid request body' }, 400);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!url) {
|
|
102
|
-
return c.json({ success: false, error: 'Missing URL' }, 400);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
const sendId = token;
|
|
107
|
-
|
|
108
|
-
// Look up the send record
|
|
109
|
-
const send = await db
|
|
110
|
-
.prepare(`SELECT * FROM ${tablePrefix}sends WHERE send_id = ?`)
|
|
111
|
-
.bind(sendId)
|
|
112
|
-
.first();
|
|
113
|
-
|
|
114
|
-
if (send) {
|
|
115
|
-
// Log the click event
|
|
116
|
-
const eventId = crypto.randomUUID();
|
|
117
|
-
const now = Math.floor(Date.now() / 1000);
|
|
118
|
-
|
|
119
|
-
await db
|
|
120
|
-
.prepare(
|
|
121
|
-
`INSERT INTO ${tablePrefix}events (
|
|
122
|
-
event_id, send_id, user_id, tenant_id, email_kind, event_type, metadata, created_at
|
|
123
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
124
|
-
)
|
|
125
|
-
.bind(
|
|
126
|
-
eventId,
|
|
127
|
-
sendId,
|
|
128
|
-
send.user_id,
|
|
129
|
-
send.tenant_id,
|
|
130
|
-
send.email_kind,
|
|
131
|
-
'clicked',
|
|
132
|
-
JSON.stringify({
|
|
133
|
-
url: url,
|
|
134
|
-
user_agent: c.req.header('user-agent'),
|
|
135
|
-
referer: c.req.header('referer'),
|
|
136
|
-
}),
|
|
137
|
-
now
|
|
138
|
-
)
|
|
139
|
-
.run();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return c.json({ success: true, tracked: !!send });
|
|
143
|
-
} catch (error) {
|
|
144
|
-
console.error('[EmailTracking] Error tracking email click:', error);
|
|
145
|
-
return c.json({ success: false, error: 'Failed to track click' }, 500);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* GET /unsubscribe/:token
|
|
151
|
-
* Unsubscribe a user from all email notifications
|
|
152
|
-
*/
|
|
153
|
-
app.get('/unsubscribe/:token', async (c) => {
|
|
154
|
-
const unsubToken = c.req.param('token');
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
// Find the user by their unsubscribe token
|
|
158
|
-
const prefs = await db
|
|
159
|
-
.prepare(`SELECT * FROM ${tablePrefix}preferences WHERE unsub_token = ?`)
|
|
160
|
-
.bind(unsubToken)
|
|
161
|
-
.first();
|
|
162
|
-
|
|
163
|
-
if (!prefs) {
|
|
164
|
-
return c.json({ success: false, error: 'Invalid unsubscribe link' }, 404);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Check if already unsubscribed
|
|
168
|
-
const currentSettings = JSON.parse(prefs.email_settings || '{}');
|
|
169
|
-
const alreadyUnsubscribed = Object.keys(currentSettings).length === 0;
|
|
170
|
-
|
|
171
|
-
if (!alreadyUnsubscribed) {
|
|
172
|
-
// Disable all email types by setting email_settings to empty object
|
|
173
|
-
const now = Math.floor(Date.now() / 1000);
|
|
174
|
-
await db
|
|
175
|
-
.prepare(
|
|
176
|
-
`UPDATE ${tablePrefix}preferences
|
|
177
|
-
SET email_settings = '{}',
|
|
178
|
-
updated_at = ?
|
|
179
|
-
WHERE unsub_token = ?`
|
|
180
|
-
)
|
|
181
|
-
.bind(now, unsubToken)
|
|
182
|
-
.run();
|
|
183
|
-
|
|
184
|
-
// Log the unsubscribe event
|
|
185
|
-
const eventId = crypto.randomUUID();
|
|
186
|
-
await db
|
|
187
|
-
.prepare(
|
|
188
|
-
`INSERT INTO ${tablePrefix}events (
|
|
189
|
-
event_id, send_id, user_id, tenant_id, email_kind, event_type, metadata, created_at
|
|
190
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
191
|
-
)
|
|
192
|
-
.bind(
|
|
193
|
-
eventId,
|
|
194
|
-
'unsubscribe',
|
|
195
|
-
prefs.user_id,
|
|
196
|
-
prefs.tenant_id,
|
|
197
|
-
'all',
|
|
198
|
-
'unsubscribed',
|
|
199
|
-
JSON.stringify({
|
|
200
|
-
user_agent: c.req.header('user-agent'),
|
|
201
|
-
}),
|
|
202
|
-
now
|
|
203
|
-
)
|
|
204
|
-
.run();
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return c.json({ success: true, alreadyUnsubscribed });
|
|
208
|
-
} catch (error) {
|
|
209
|
-
console.error('[EmailTracking] Error processing unsubscribe:', error);
|
|
210
|
-
return c.json({ success: false, error: 'An error occurred' }, 500);
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
return app;
|
|
215
|
-
}
|
package/src/backend/routes.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Email Routes Factory
|
|
3
|
-
* Creates a Hono router with admin endpoints for the email system.
|
|
4
|
-
*/
|
|
5
|
-
import { Hono } from 'hono';
|
|
6
|
-
import { EmailService } from './EmailService';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Create email admin routes
|
|
10
|
-
* @param {Object} env - Environment bindings
|
|
11
|
-
* @param {Object} config - Configuration
|
|
12
|
-
* @param {Object} cacheProvider - Optional cache provider
|
|
13
|
-
* @returns {Hono} Hono router
|
|
14
|
-
*/
|
|
15
|
-
export function createEmailRoutes(env, config = {}, cacheProvider = null) {
|
|
16
|
-
const app = new Hono();
|
|
17
|
-
const emailService = new EmailService(env, config, cacheProvider);
|
|
18
|
-
|
|
19
|
-
// List all templates
|
|
20
|
-
app.get('/templates', async (c) => {
|
|
21
|
-
try {
|
|
22
|
-
const templates = await emailService.getAllTemplates();
|
|
23
|
-
return c.json({ success: true, templates });
|
|
24
|
-
} catch (err) {
|
|
25
|
-
return c.json({ success: false, error: err.message }, 500);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// Get single template
|
|
30
|
-
app.get('/templates/:id', async (c) => {
|
|
31
|
-
try {
|
|
32
|
-
const template = await emailService.getTemplate(c.req.param('id'));
|
|
33
|
-
if (!template) return c.json({ success: false, error: 'Template not found' }, 404);
|
|
34
|
-
return c.json({ success: true, template });
|
|
35
|
-
} catch (err) {
|
|
36
|
-
return c.json({ success: false, error: err.message }, 500);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Create/Update template
|
|
41
|
-
app.post('/templates', async (c) => {
|
|
42
|
-
try {
|
|
43
|
-
const data = await c.req.json();
|
|
44
|
-
await emailService.saveTemplate(data, 'admin'); // TODO: Pass real user ID
|
|
45
|
-
return c.json({ success: true, message: 'Template saved' });
|
|
46
|
-
} catch (err) {
|
|
47
|
-
return c.json({ success: false, error: err.message }, 500);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// Delete template
|
|
52
|
-
app.delete('/templates/:id', async (c) => {
|
|
53
|
-
try {
|
|
54
|
-
await emailService.deleteTemplate(c.req.param('id'));
|
|
55
|
-
return c.json({ success: true, message: 'Template deleted' });
|
|
56
|
-
} catch (err) {
|
|
57
|
-
return c.json({ success: false, error: err.message }, 500);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// Preview template
|
|
62
|
-
app.post('/templates/:id/preview', async (c) => {
|
|
63
|
-
try {
|
|
64
|
-
const id = c.req.param('id');
|
|
65
|
-
const data = await c.req.json(); // Variables
|
|
66
|
-
const result = await emailService.renderTemplate(id, data);
|
|
67
|
-
return c.json({ success: true, preview: result });
|
|
68
|
-
} catch (err) {
|
|
69
|
-
return c.json({ success: false, error: err.message }, 500);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Send test email
|
|
74
|
-
app.post('/templates/:id/test', async (c) => {
|
|
75
|
-
try {
|
|
76
|
-
const id = c.req.param('id');
|
|
77
|
-
const { to, data } = await c.req.json();
|
|
78
|
-
|
|
79
|
-
const { subject, html, plainText } = await emailService.renderTemplate(id, data);
|
|
80
|
-
const result = await emailService.sendEmail({
|
|
81
|
-
to,
|
|
82
|
-
subject: `[TEST] ${subject}`,
|
|
83
|
-
html,
|
|
84
|
-
text: plainText
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
if (result.success) {
|
|
88
|
-
return c.json({ success: true, message: 'Test email sent' });
|
|
89
|
-
} else {
|
|
90
|
-
return c.json({ success: false, error: result.error }, 500);
|
|
91
|
-
}
|
|
92
|
-
} catch (err) {
|
|
93
|
-
return c.json({ success: false, error: err.message }, 500);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
return app;
|
|
98
|
-
}
|
package/src/common/index.js
DELETED
package/src/common/utils.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Email Utility Functions
|
|
3
|
-
* Common helpers for email processing
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import mustache from 'mustache';
|
|
7
|
-
|
|
8
|
-
// Module-level cache for website URL
|
|
9
|
-
let cachedWebsiteUrl = null;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Build website URL based on environment configuration (with caching)
|
|
13
|
-
* @param {Object} env - Environment bindings
|
|
14
|
-
* @returns {string} Website URL (e.g., 'https://www.x0start.com')
|
|
15
|
-
*/
|
|
16
|
-
export function getWebsiteUrl(env) {
|
|
17
|
-
if (cachedWebsiteUrl) {
|
|
18
|
-
return cachedWebsiteUrl;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const domain = env.DOMAIN || 'x0start.com';
|
|
22
|
-
const isDev = env.ENVIRONMENT === 'development' || !env.ENVIRONMENT;
|
|
23
|
-
const protocol = isDev ? 'http' : 'https';
|
|
24
|
-
|
|
25
|
-
cachedWebsiteUrl = `${protocol}://www.${domain}`;
|
|
26
|
-
|
|
27
|
-
return cachedWebsiteUrl;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Reset the cached website URL (for testing or config changes)
|
|
32
|
-
*/
|
|
33
|
-
export function resetWebsiteUrlCache() {
|
|
34
|
-
cachedWebsiteUrl = null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Encode all links in HTML to go through tracking redirect
|
|
39
|
-
* @param {string} html - HTML content with links
|
|
40
|
-
* @param {string} sendId - Unique send ID for tracking
|
|
41
|
-
* @param {Object} env - Environment bindings (optional, for auto-detecting URL)
|
|
42
|
-
* @param {string} websiteUrl - Base website URL (optional, overrides env detection)
|
|
43
|
-
* @returns {string} HTML with encoded tracking links
|
|
44
|
-
*/
|
|
45
|
-
export function encodeTrackingLinks(html, sendId, env = null, websiteUrl = null) {
|
|
46
|
-
const baseUrl = websiteUrl || (env ? getWebsiteUrl(env) : 'https://www.x0start.com');
|
|
47
|
-
|
|
48
|
-
// Helper to decode HTML entities
|
|
49
|
-
const decodeHtmlEntities = (text) => {
|
|
50
|
-
return text
|
|
51
|
-
.replace(///g, '/')
|
|
52
|
-
.replace(/:/g, ':')
|
|
53
|
-
.replace(/&/g, '&')
|
|
54
|
-
.replace(/</g, '<')
|
|
55
|
-
.replace(/>/g, '>')
|
|
56
|
-
.replace(/"/g, '"')
|
|
57
|
-
.replace(/'/g, "'");
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// Replace all href attributes with tracking URLs
|
|
61
|
-
return html.replace(/href="([^"]+)"/g, (match, url) => {
|
|
62
|
-
// Skip certain URLs that shouldn't be tracked
|
|
63
|
-
if (
|
|
64
|
-
url.startsWith('mailto:') ||
|
|
65
|
-
url.startsWith('tel:') ||
|
|
66
|
-
url.startsWith('#') ||
|
|
67
|
-
url.includes('{{') || // Skip template variables
|
|
68
|
-
url.includes('/email/unsubscribe/') ||
|
|
69
|
-
url.includes('/email/track/')
|
|
70
|
-
) {
|
|
71
|
-
return match;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Decode HTML entities first (marked encodes URLs with HTML entities)
|
|
75
|
-
const decodedUrl = decodeHtmlEntities(url);
|
|
76
|
-
|
|
77
|
-
// Encode the decoded URL in base64
|
|
78
|
-
const encodedUrl = Buffer.from(decodedUrl).toString('base64');
|
|
79
|
-
|
|
80
|
-
// Create tracking redirect URL
|
|
81
|
-
const trackingUrl = `${baseUrl}/r/${sendId}?url=${encodedUrl}`;
|
|
82
|
-
|
|
83
|
-
return `href="${trackingUrl}"`;
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Convert markdown to plain text
|
|
89
|
-
* @param {string} markdown - Markdown content
|
|
90
|
-
* @returns {string} Plain text version
|
|
91
|
-
*/
|
|
92
|
-
export function markdownToPlainText(markdown) {
|
|
93
|
-
return markdown
|
|
94
|
-
.replace(/#{1,6}\s+/g, '') // Remove headers
|
|
95
|
-
.replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold
|
|
96
|
-
.replace(/\*(.+?)\*/g, '$1') // Remove italic
|
|
97
|
-
.replace(/\[(.+?)\]\((.+?)\)/g, '$1: $2') // Keep both link text and URL
|
|
98
|
-
.replace(/`(.+?)`/g, '$1') // Remove code
|
|
99
|
-
.replace(/>\s+/g, '') // Remove blockquotes
|
|
100
|
-
.replace(/\n{3,}/g, '\n\n') // Normalize line breaks
|
|
101
|
-
.trim();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Extract variables from a Mustache template string
|
|
106
|
-
* @param {string} templateString - The mustache template string
|
|
107
|
-
* @returns {string[]} Array of unique variable names
|
|
108
|
-
*/
|
|
109
|
-
export function extractVariables(templateString) {
|
|
110
|
-
if (!templateString) return [];
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
// Parse the template to get tokens
|
|
114
|
-
const tokens = mustache.parse(templateString);
|
|
115
|
-
const variables = new Set();
|
|
116
|
-
|
|
117
|
-
// Recursively extract variables from tokens
|
|
118
|
-
const collectVariables = (tokenList) => {
|
|
119
|
-
tokenList.forEach(token => {
|
|
120
|
-
const type = token[0];
|
|
121
|
-
const value = token[1];
|
|
122
|
-
|
|
123
|
-
// Types: 'name' ({{var}}), '#' (section), '^' (inverted section), '&' (unescaped)
|
|
124
|
-
if (type === 'name' || type === '#' || type === '^' || type === '&') {
|
|
125
|
-
variables.add(value);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Variable is in token[4] for sections
|
|
129
|
-
if ((type === '#' || type === '^') && token[4]) {
|
|
130
|
-
collectVariables(token[4]);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
collectVariables(tokens);
|
|
136
|
-
return Array.from(variables);
|
|
137
|
-
} catch (error) {
|
|
138
|
-
console.error('Error parsing template variables:', error);
|
|
139
|
-
return [];
|
|
140
|
-
}
|
|
141
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { Save, X, Eye, AlertCircle } from 'lucide-react';
|
|
3
|
-
|
|
4
|
-
export const TemplateEditor = ({
|
|
5
|
-
initialData,
|
|
6
|
-
onSave,
|
|
7
|
-
onCancel,
|
|
8
|
-
onPreview,
|
|
9
|
-
variablesHelpText = 'Supports {{variable}}'
|
|
10
|
-
}) => {
|
|
11
|
-
const [formData, setFormData] = useState({
|
|
12
|
-
template_id: '',
|
|
13
|
-
template_name: '',
|
|
14
|
-
template_type: 'daily_reminder',
|
|
15
|
-
subject_template: '',
|
|
16
|
-
body_markdown: '',
|
|
17
|
-
variables: '[]',
|
|
18
|
-
description: '',
|
|
19
|
-
is_active: 1,
|
|
20
|
-
...initialData
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const handleSubmit = (e) => {
|
|
24
|
-
e.preventDefault();
|
|
25
|
-
onSave(formData);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
30
|
-
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
|
31
|
-
<div className="p-6 border-b border-gray-200 flex justify-between items-center">
|
|
32
|
-
<h2 className="text-xl font-bold text-gray-900">
|
|
33
|
-
{initialData ? 'Edit Template' : 'New Template'}
|
|
34
|
-
</h2>
|
|
35
|
-
<button onClick={onCancel} className="p-2 hover:bg-gray-100 rounded-lg">
|
|
36
|
-
<X className="h-5 w-5" />
|
|
37
|
-
</button>
|
|
38
|
-
</div>
|
|
39
|
-
|
|
40
|
-
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-6 space-y-4">
|
|
41
|
-
<div className="grid grid-cols-2 gap-4">
|
|
42
|
-
<div>
|
|
43
|
-
<label className="block text-sm font-medium text-gray-700 mb-1">Template ID *</label>
|
|
44
|
-
<input
|
|
45
|
-
type="text"
|
|
46
|
-
required
|
|
47
|
-
disabled={!!initialData}
|
|
48
|
-
value={formData.template_id}
|
|
49
|
-
onChange={e => setFormData({ ...formData, template_id: e.target.value })}
|
|
50
|
-
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
|
51
|
-
/>
|
|
52
|
-
</div>
|
|
53
|
-
<div>
|
|
54
|
-
<label className="block text-sm font-medium text-gray-700 mb-1">Name *</label>
|
|
55
|
-
<input
|
|
56
|
-
type="text"
|
|
57
|
-
required
|
|
58
|
-
value={formData.template_name}
|
|
59
|
-
onChange={e => setFormData({ ...formData, template_name: e.target.value })}
|
|
60
|
-
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
61
|
-
/>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<div>
|
|
66
|
-
<label className="block text-sm font-medium text-gray-700 mb-1">Subject *</label>
|
|
67
|
-
<input
|
|
68
|
-
type="text"
|
|
69
|
-
required
|
|
70
|
-
value={formData.subject_template}
|
|
71
|
-
onChange={e => setFormData({ ...formData, subject_template: e.target.value })}
|
|
72
|
-
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
73
|
-
/>
|
|
74
|
-
<p className="text-xs text-gray-500 mt-1">{variablesHelpText}</p>
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
<div>
|
|
78
|
-
<label className="block text-sm font-medium text-gray-700 mb-1">Body (Markdown) *</label>
|
|
79
|
-
<textarea
|
|
80
|
-
required
|
|
81
|
-
rows={12}
|
|
82
|
-
value={formData.body_markdown}
|
|
83
|
-
onChange={e => setFormData({ ...formData, body_markdown: e.target.value })}
|
|
84
|
-
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
|
85
|
-
/>
|
|
86
|
-
</div>
|
|
87
|
-
|
|
88
|
-
<div className="flex justify-between pt-4">
|
|
89
|
-
<button
|
|
90
|
-
type="button"
|
|
91
|
-
onClick={() => onPreview && onPreview(formData)}
|
|
92
|
-
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
|
|
93
|
-
>
|
|
94
|
-
<Eye className="h-4 w-4" /> Preview
|
|
95
|
-
</button>
|
|
96
|
-
|
|
97
|
-
<div className="flex gap-2">
|
|
98
|
-
<button
|
|
99
|
-
type="button"
|
|
100
|
-
onClick={onCancel}
|
|
101
|
-
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
|
|
102
|
-
>
|
|
103
|
-
Cancel
|
|
104
|
-
</button>
|
|
105
|
-
<button
|
|
106
|
-
type="submit"
|
|
107
|
-
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
|
108
|
-
>
|
|
109
|
-
<Save className="h-4 w-4" /> Save
|
|
110
|
-
</button>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
</form>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
);
|
|
117
|
-
};
|