universal-email-templates-cli 1.0.0 โ†’ 1.0.1

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/README.md CHANGED
@@ -60,6 +60,22 @@ email-export --platform klaviyo --src ./dist/html --dist ./dist/klaviyo
60
60
  email-export --platform hubspot --src ./dist/html --dist ./dist/hubspot
61
61
  ```
62
62
 
63
+ ### Generate Preview Examples
64
+
65
+ ```bash
66
+ # Generate examples for all languages
67
+ email-examples --dist ./dist --examples ./examples --config ./config/examples
68
+
69
+ # Generate English only
70
+ email-examples en --dist ./dist --examples ./examples --config ./config/examples
71
+
72
+ # Generate French only
73
+ email-examples fr --dist ./dist --examples ./examples --config ./config/examples
74
+
75
+ # Clean and regenerate
76
+ email-examples --clean --dist ./dist --examples ./examples --config ./config/examples
77
+ ```
78
+
63
79
  ## Programmatic Usage
64
80
 
65
81
  ```javascript
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI: Generate preview examples with real data
5
+ * Usage: email-examples --dist ./dist --examples ./examples --config ./config/examples
6
+ */
7
+
8
+ const path = require('path');
9
+ const { generateExamples } = require('../lib/examples');
10
+
11
+ // Parse command line arguments
12
+ const args = process.argv.slice(2);
13
+
14
+ function getArg(name, defaultValue) {
15
+ const index = args.indexOf(`--${name}`);
16
+ if (index !== -1 && args[index + 1]) {
17
+ return args[index + 1];
18
+ }
19
+ return defaultValue;
20
+ }
21
+
22
+ function hasFlag(name) {
23
+ return args.includes(`--${name}`) || args.includes(`-${name[0]}`);
24
+ }
25
+
26
+ // Get specific language if provided as first non-flag argument
27
+ function getSpecificLang() {
28
+ const lang = args.find(a => !a.startsWith('--') && !a.startsWith('-'));
29
+ return lang;
30
+ }
31
+
32
+ // Show help
33
+ if (hasFlag('help') || hasFlag('h')) {
34
+ console.log(`
35
+ ๐Ÿ“‹ Universal Email Templates - Examples Generator CLI
36
+
37
+ Usage:
38
+ email-examples [options] [language]
39
+
40
+ Options:
41
+ --dist <path> Directory with built HTML files (default: ./dist)
42
+ --examples <path> Output directory for examples (default: ./examples)
43
+ --config <path> Directory with example data configs (default: ./config/examples)
44
+ --clean Clean examples folder before generating
45
+ --silent Suppress console output
46
+ --help, -h Show this help message
47
+
48
+ Languages:
49
+ en Generate English examples only
50
+ fr Generate French examples only
51
+ (none) Generate all languages (en, fr)
52
+
53
+ Config Files Required:
54
+ <config>/en.json English example data
55
+ <config>/fr.json French example data
56
+ <config>/images.json Image URLs configuration
57
+
58
+ Examples:
59
+ email-examples
60
+ email-examples en
61
+ email-examples fr --clean
62
+ email-examples --dist ./dist --examples ./output --config ./data
63
+ `);
64
+ process.exit(0);
65
+ }
66
+
67
+ // Run generator
68
+ const specificLang = getSpecificLang();
69
+ const languages = specificLang ? [specificLang] : ['en', 'fr'];
70
+
71
+ const options = {
72
+ distDir: path.resolve(getArg('dist', './dist')),
73
+ examplesDir: path.resolve(getArg('examples', './examples')),
74
+ configDir: path.resolve(getArg('config', './config/examples')),
75
+ languages,
76
+ clean: hasFlag('clean'),
77
+ silent: hasFlag('silent')
78
+ };
79
+
80
+ generateExamples(options)
81
+ .then(result => {
82
+ if (result.errors > 0) {
83
+ process.exit(1);
84
+ }
85
+ })
86
+ .catch(error => {
87
+ console.error('โŒ Example generation failed:', error.message);
88
+ process.exit(1);
89
+ });
package/index.js CHANGED
@@ -1,17 +1,19 @@
1
1
  /**
2
2
  * Universal Email Templates CLI
3
- * Build, translate, and export MJML email templates
3
+ * Build, translate, export and generate examples for MJML email templates
4
4
  */
5
5
 
6
6
  const { build } = require('./lib/builder');
7
7
  const { translate, applyTranslations } = require('./lib/translator');
8
8
  const { exportTo, platforms } = require('./lib/exporter');
9
+ const { generateExamples } = require('./lib/examples');
9
10
 
10
11
  module.exports = {
11
12
  // Main functions
12
13
  build,
13
14
  translate,
14
15
  exportTo,
16
+ generateExamples,
15
17
 
16
18
  // Utilities
17
19
  applyTranslations,
@@ -0,0 +1,563 @@
1
+ /**
2
+ * Examples Generator
3
+ * Creates preview-ready HTML files with all variables replaced by example data
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const fse = require('fs-extra');
8
+ const path = require('path');
9
+ const glob = require('glob');
10
+
11
+ /**
12
+ * UI Translations for index.html
13
+ */
14
+ const uiTranslations = {
15
+ en: {
16
+ title: 'Universal Email Templates',
17
+ subtitle: 'Professional MJML Email Templates - Ready to Use',
18
+ templates: 'Templates',
19
+ categories: 'Categories',
20
+ responsive: 'Responsive',
21
+ searchPlaceholder: 'Search templates...',
22
+ preview: 'Preview',
23
+ footer: 'Universal Email Templates Pack - Professional MJML Templates',
24
+ cat_core: 'Core Templates',
25
+ cat_ecommerce: 'E-Commerce',
26
+ cat_saas: 'SaaS',
27
+ cat_agency: 'Agency',
28
+ cat_restaurant: 'Restaurant',
29
+ cat_fitness: 'Fitness',
30
+ cat_education: 'Education',
31
+ cat_realestate: 'Real Estate',
32
+ cat_nonprofit: 'Nonprofit',
33
+ cat_travel: 'Travel',
34
+ cat_seasonal: 'Seasonal',
35
+ cat_events: 'Events',
36
+ templatesCount: 'templates'
37
+ },
38
+ fr: {
39
+ title: 'Templates Email Universels',
40
+ subtitle: 'Templates Email MJML Professionnels - Prets a utiliser',
41
+ templates: 'Templates',
42
+ categories: 'Categories',
43
+ responsive: 'Responsive',
44
+ searchPlaceholder: 'Rechercher des templates...',
45
+ preview: 'Apercu',
46
+ footer: 'Pack de Templates Email Universels - Templates MJML Professionnels',
47
+ cat_core: 'Templates Essentiels',
48
+ cat_ecommerce: 'E-Commerce',
49
+ cat_saas: 'SaaS',
50
+ cat_agency: 'Agence',
51
+ cat_restaurant: 'Restaurant',
52
+ cat_fitness: 'Fitness',
53
+ cat_education: 'Education',
54
+ cat_realestate: 'Immobilier',
55
+ cat_nonprofit: 'Association',
56
+ cat_travel: 'Voyage',
57
+ cat_seasonal: 'Saisonnier',
58
+ cat_events: 'Evenements',
59
+ templatesCount: 'templates'
60
+ }
61
+ };
62
+
63
+ // Image counters for cycling through arrays
64
+ let imageCounters = {};
65
+
66
+ /**
67
+ * Get image URL based on context and template type
68
+ */
69
+ function getImageUrl(context, templatePath, imagesConfig) {
70
+ if (!imagesConfig) {
71
+ return 'https://images.unsplash.com/photo-1557200134-90327ee9fafa?w=600&h=400&fit=crop';
72
+ }
73
+
74
+ const pathLower = templatePath.toLowerCase();
75
+ let category = 'heroes';
76
+ let subcategory = 'welcome';
77
+
78
+ if (pathLower.includes('ecommerce') || pathLower.includes('cart') || pathLower.includes('product')) {
79
+ category = context.includes('product') ? 'products' : 'shopping';
80
+ subcategory = 'bags';
81
+ } else if (pathLower.includes('travel')) {
82
+ category = 'travel';
83
+ subcategory = 'luggage';
84
+ } else if (pathLower.includes('restaurant') || pathLower.includes('food')) {
85
+ category = 'food';
86
+ } else if (pathLower.includes('fitness') || pathLower.includes('gym')) {
87
+ category = 'fitness';
88
+ subcategory = 'gym';
89
+ } else if (pathLower.includes('education') || pathLower.includes('course')) {
90
+ category = 'education';
91
+ subcategory = 'classroom';
92
+ } else if (pathLower.includes('realestate') || pathLower.includes('property')) {
93
+ category = 'property';
94
+ subcategory = 'modern_house';
95
+ } else if (pathLower.includes('charity') || pathLower.includes('nonprofit')) {
96
+ category = 'charity';
97
+ subcategory = 'volunteers';
98
+ } else if (pathLower.includes('agency') || pathLower.includes('project')) {
99
+ category = 'agency';
100
+ subcategory = 'meeting';
101
+ } else if (pathLower.includes('newsletter')) {
102
+ category = 'newsletter';
103
+ subcategory = 'laptop';
104
+ } else if (pathLower.includes('seasonal') || pathLower.includes('holiday')) {
105
+ category = 'holiday';
106
+ subcategory = 'gifts';
107
+ } else if (pathLower.includes('saas') || pathLower.includes('dashboard')) {
108
+ category = 'dashboard';
109
+ subcategory = 'analytics';
110
+ } else if (pathLower.includes('team') || pathLower.includes('welcome')) {
111
+ category = 'team';
112
+ subcategory = 'meeting';
113
+ }
114
+
115
+ if (context.includes('avatar') || context.includes('profile') || context.includes('trainer')) {
116
+ category = 'avatars';
117
+ }
118
+
119
+ if (context.includes('logo')) {
120
+ return imagesConfig.logos?.default || 'https://via.placeholder.com/200x60?text=Logo';
121
+ }
122
+
123
+ const imageSet = imagesConfig[category];
124
+ if (!imageSet) {
125
+ return imagesConfig.heroes?.welcome || 'https://images.unsplash.com/photo-1557200134-90327ee9fafa?w=600&h=400&fit=crop';
126
+ }
127
+
128
+ if (Array.isArray(imageSet)) {
129
+ if (!imageCounters[category]) imageCounters[category] = 0;
130
+ const index = imageCounters[category] % imageSet.length;
131
+ imageCounters[category]++;
132
+ return imageSet[index];
133
+ }
134
+
135
+ if (typeof imageSet === 'object' && subcategory && imageSet[subcategory]) {
136
+ return imageSet[subcategory];
137
+ }
138
+
139
+ if (typeof imageSet === 'object') {
140
+ return Object.values(imageSet)[0];
141
+ }
142
+
143
+ return imageSet;
144
+ }
145
+
146
+ /**
147
+ * Flatten nested object to dot notation
148
+ */
149
+ function flattenObject(obj, prefix = '') {
150
+ const result = {};
151
+ for (const key in obj) {
152
+ const newKey = prefix ? `${prefix}_${key}` : key;
153
+ if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
154
+ Object.assign(result, flattenObject(obj[key], newKey));
155
+ } else {
156
+ result[newKey] = obj[key];
157
+ }
158
+ }
159
+ return result;
160
+ }
161
+
162
+ /**
163
+ * Replace all variables in content
164
+ */
165
+ function replaceVariables(content, data, templatePath, imagesConfig) {
166
+ let result = content;
167
+ const flatData = flattenObject(data);
168
+
169
+ for (const section in data) {
170
+ if (typeof data[section] === 'object') {
171
+ for (const key in data[section]) {
172
+ flatData[key] = data[section][key];
173
+ }
174
+ }
175
+ }
176
+
177
+ result = result.replace(/\{\{([a-zA-Z0-9_]+)\}\}/g, (match, varName) => {
178
+ if (varName.includes('image') || varName.includes('logo') || varName.includes('avatar') || varName.includes('photo')) {
179
+ return getImageUrl(varName, templatePath, imagesConfig);
180
+ }
181
+ if (flatData[varName] !== undefined) {
182
+ return flatData[varName];
183
+ }
184
+ const cleanName = varName.replace(/^(company_|user_|order_|item\d*_|product\d*_|shipping_|billing_)/, '');
185
+ if (flatData[cleanName] !== undefined) {
186
+ return flatData[cleanName];
187
+ }
188
+ return `[${varName}]`;
189
+ });
190
+
191
+ return result;
192
+ }
193
+
194
+ /**
195
+ * Get category info
196
+ */
197
+ function getCategoryInfo(category, lang = 'en') {
198
+ const t = uiTranslations[lang] || uiTranslations.en;
199
+ const categoryMap = {
200
+ 'core': { emoji: '๐Ÿ“ง', name: t.cat_core },
201
+ 'ecommerce': { emoji: '๐Ÿ›’', name: t.cat_ecommerce },
202
+ 'saas': { emoji: '๐Ÿ’ป', name: t.cat_saas },
203
+ 'agency': { emoji: '๐Ÿข', name: t.cat_agency },
204
+ 'restaurant': { emoji: '๐Ÿฝ๏ธ', name: t.cat_restaurant },
205
+ 'fitness': { emoji: '๐Ÿ’ช', name: t.cat_fitness },
206
+ 'education': { emoji: '๐Ÿ“š', name: t.cat_education },
207
+ 'realestate': { emoji: '๐Ÿ ', name: t.cat_realestate },
208
+ 'nonprofit': { emoji: 'โค๏ธ', name: t.cat_nonprofit },
209
+ 'travel': { emoji: 'โœˆ๏ธ', name: t.cat_travel },
210
+ 'seasonal': { emoji: '๐ŸŽ„', name: t.cat_seasonal },
211
+ 'events': { emoji: '๐Ÿ“…', name: t.cat_events }
212
+ };
213
+ return categoryMap[category] || { emoji: '๐Ÿ“', name: category.charAt(0).toUpperCase() + category.slice(1) };
214
+ }
215
+
216
+ /**
217
+ * Process templates for a specific language
218
+ */
219
+ async function processLanguage(options) {
220
+ const { lang, sourceDir, outputDir, exampleData, imagesConfig, silent } = options;
221
+
222
+ if (!fs.existsSync(sourceDir)) {
223
+ if (!silent) {
224
+ console.error(`Source directory not found: ${sourceDir}`);
225
+ console.log('Run build first to generate HTML templates.');
226
+ }
227
+ return { success: 0, errors: 0 };
228
+ }
229
+
230
+ await fse.ensureDir(outputDir);
231
+
232
+ const htmlFiles = glob.sync('**/*.html', { cwd: sourceDir });
233
+
234
+ let success = 0;
235
+ let errors = 0;
236
+
237
+ if (!silent) {
238
+ console.log(`\n๐Ÿ“ Processing ${lang.toUpperCase()} templates...`);
239
+ }
240
+
241
+ for (const file of htmlFiles) {
242
+ const sourcePath = path.join(sourceDir, file);
243
+ const outputPath = path.join(outputDir, file);
244
+
245
+ try {
246
+ const content = fs.readFileSync(sourcePath, 'utf-8');
247
+ imageCounters = {};
248
+ const processedContent = replaceVariables(content, exampleData, file, imagesConfig);
249
+
250
+ await fse.ensureDir(path.dirname(outputPath));
251
+ fs.writeFileSync(outputPath, processedContent);
252
+
253
+ if (!silent) {
254
+ console.log(` โœ… ${file}`);
255
+ }
256
+ success++;
257
+ } catch (err) {
258
+ if (!silent) {
259
+ console.error(` โŒ ${file}: ${err.message}`);
260
+ }
261
+ errors++;
262
+ }
263
+ }
264
+
265
+ return { success, errors };
266
+ }
267
+
268
+ /**
269
+ * Create index.html with links to all examples
270
+ */
271
+ function createIndex(examplesDir, processedLangs, templatesByLang) {
272
+ let totalTemplates = 0;
273
+ const allCategories = new Set();
274
+
275
+ for (const lang of processedLangs) {
276
+ const data = templatesByLang[lang];
277
+ if (!data) continue;
278
+ Object.keys(data.categories).forEach(cat => allCategories.add(cat));
279
+ if (lang === 'en') totalTemplates = data.files.length;
280
+ }
281
+
282
+ const categoryCount = allCategories.size;
283
+ const t = uiTranslations.en;
284
+ const tFr = uiTranslations.fr;
285
+
286
+ // Generate HTML (simplified version)
287
+ let html = `<!DOCTYPE html>
288
+ <html lang="en">
289
+ <head>
290
+ <meta charset="UTF-8">
291
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
292
+ <title>Universal Email Templates</title>
293
+ <style>
294
+ * { box-sizing: border-box; margin: 0; padding: 0; }
295
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); min-height: 100vh; color: #e2e8f0; }
296
+ .hero { text-align: center; padding: 60px 20px 40px; background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%); }
297
+ .hero h1 { font-size: 42px; font-weight: 700; color: #fff; margin-bottom: 12px; }
298
+ .hero .subtitle { font-size: 18px; color: #94a3b8; margin-bottom: 40px; }
299
+ .stats { display: flex; justify-content: center; gap: 40px; margin-bottom: 40px; flex-wrap: wrap; }
300
+ .stat-card { background: rgba(30, 58, 95, 0.5); border: 1px solid rgba(59, 130, 246, 0.3); border-radius: 16px; padding: 24px 40px; text-align: center; }
301
+ .stat-number { font-size: 36px; font-weight: 700; color: #3b82f6; display: block; }
302
+ .stat-label { font-size: 14px; color: #94a3b8; text-transform: uppercase; }
303
+ .search-container { max-width: 500px; margin: 0 auto; position: relative; }
304
+ .search-input { width: 100%; padding: 16px 24px 16px 50px; font-size: 16px; border: 2px solid rgba(59, 130, 246, 0.3); border-radius: 50px; background: rgba(15, 23, 42, 0.8); color: #e2e8f0; outline: none; }
305
+ .search-icon { position: absolute; left: 20px; top: 50%; transform: translateY(-50%); color: #64748b; }
306
+ .lang-tabs { display: flex; justify-content: center; gap: 10px; padding: 20px; background: rgba(15, 23, 42, 0.5); }
307
+ .lang-tab { padding: 12px 30px; font-size: 15px; font-weight: 600; border: 2px solid rgba(59, 130, 246, 0.3); border-radius: 50px; background: transparent; color: #94a3b8; cursor: pointer; }
308
+ .lang-tab:hover { border-color: #3b82f6; color: #e2e8f0; }
309
+ .lang-tab.active { background: #3b82f6; border-color: #3b82f6; color: #fff; }
310
+ .container { max-width: 1400px; margin: 0 auto; padding: 40px 20px; }
311
+ .language-content { display: none; }
312
+ .language-content.active { display: block; }
313
+ .category-section { margin-bottom: 50px; }
314
+ .category-header { display: flex; align-items: center; gap: 12px; margin-bottom: 24px; padding-bottom: 12px; border-bottom: 1px solid rgba(59, 130, 246, 0.2); }
315
+ .category-emoji { font-size: 28px; }
316
+ .category-title { font-size: 24px; font-weight: 600; color: #fff; }
317
+ .category-count { font-size: 14px; color: #64748b; background: rgba(59, 130, 246, 0.2); padding: 4px 12px; border-radius: 20px; margin-left: auto; }
318
+ .templates-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; }
319
+ @media (max-width: 1200px) { .templates-grid { grid-template-columns: repeat(3, 1fr); } }
320
+ @media (max-width: 900px) { .templates-grid { grid-template-columns: repeat(2, 1fr); } }
321
+ @media (max-width: 600px) { .templates-grid { grid-template-columns: 1fr; } }
322
+ .template-card { background: rgba(30, 41, 59, 0.8); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 12px; overflow: hidden; transition: transform 0.2s; }
323
+ .template-card:hover { transform: translateY(-4px); border-color: rgba(59, 130, 246, 0.5); }
324
+ .template-preview { position: relative; height: 450px; background: #fff; overflow: hidden; }
325
+ .template-preview iframe { width: 200%; height: 900px; transform: scale(0.5); transform-origin: top left; border: none; pointer-events: none; }
326
+ .template-info { padding: 16px; }
327
+ .template-name { font-size: 16px; font-weight: 600; color: #fff; margin-bottom: 6px; text-transform: capitalize; }
328
+ .template-path { font-size: 12px; color: #64748b; margin-bottom: 12px; font-family: monospace; }
329
+ .preview-btn { display: inline-block; width: 100%; padding: 10px 20px; font-size: 14px; font-weight: 600; text-align: center; text-decoration: none; color: #fff; background: #3b82f6; border-radius: 8px; }
330
+ .preview-btn:hover { background: #2563eb; }
331
+ .footer { text-align: center; padding: 40px 20px; border-top: 1px solid rgba(59, 130, 246, 0.2); }
332
+ .footer p { color: #64748b; font-size: 14px; }
333
+ .template-card.hidden, .category-section.hidden { display: none; }
334
+ </style>
335
+ </head>
336
+ <body>
337
+ <div class="hero">
338
+ <h1>${t.title}</h1>
339
+ <p class="subtitle">${t.subtitle}</p>
340
+ <div class="stats">
341
+ <div class="stat-card"><span class="stat-number">${totalTemplates}</span><span class="stat-label">${t.templates}</span></div>
342
+ <div class="stat-card"><span class="stat-number">${categoryCount}</span><span class="stat-label">${t.categories}</span></div>
343
+ <div class="stat-card"><span class="stat-number">100%</span><span class="stat-label">${t.responsive}</span></div>
344
+ </div>
345
+ <div class="search-container">
346
+ <span class="search-icon">๐Ÿ”</span>
347
+ <input type="text" class="search-input" id="searchInput" placeholder="${t.searchPlaceholder}">
348
+ </div>
349
+ </div>
350
+ <div class="lang-tabs">
351
+ `;
352
+
353
+ for (const lang of processedLangs) {
354
+ const langName = lang === 'en' ? '๐Ÿ‡ฌ๐Ÿ‡ง English' : '๐Ÿ‡ซ๐Ÿ‡ท Franรงais';
355
+ const activeClass = lang === 'en' ? ' active' : '';
356
+ html += ` <button class="lang-tab${activeClass}" data-lang="${lang}">${langName}</button>\n`;
357
+ }
358
+
359
+ html += ` </div>
360
+ <div class="container">
361
+ `;
362
+
363
+ for (const lang of processedLangs) {
364
+ const data = templatesByLang[lang];
365
+ if (!data) continue;
366
+
367
+ const activeClass = lang === 'en' ? ' active' : '';
368
+ html += ` <div class="language-content${activeClass}" id="content-${lang}">\n`;
369
+
370
+ const sortedCategories = Object.keys(data.categories).sort((a, b) => {
371
+ if (a === 'core') return -1;
372
+ if (b === 'core') return 1;
373
+ return a.localeCompare(b);
374
+ });
375
+
376
+ for (const category of sortedCategories) {
377
+ const catInfo = getCategoryInfo(category, lang);
378
+ const files = data.categories[category].sort();
379
+
380
+ html += ` <div class="category-section" data-category="${category}">
381
+ <div class="category-header">
382
+ <span class="category-emoji">${catInfo.emoji}</span>
383
+ <h2 class="category-title">${catInfo.name}</h2>
384
+ <span class="category-count">${files.length} ${t.templatesCount}</span>
385
+ </div>
386
+ <div class="templates-grid">
387
+ `;
388
+
389
+ for (const file of files) {
390
+ const name = path.basename(file, '.html').replace(/-/g, ' ');
391
+ const filePath = file.replace(/\\/g, '/');
392
+
393
+ html += ` <div class="template-card" data-name="${name.toLowerCase()}" data-path="${filePath.toLowerCase()}">
394
+ <div class="template-preview">
395
+ <iframe src="${lang}/${filePath}" loading="lazy" title="${name}"></iframe>
396
+ </div>
397
+ <div class="template-info">
398
+ <div class="template-name">${name}</div>
399
+ <div class="template-path">${filePath}</div>
400
+ <a href="${lang}/${filePath}" class="preview-btn" target="_blank">${t.preview}</a>
401
+ </div>
402
+ </div>
403
+ `;
404
+ }
405
+
406
+ html += ` </div>
407
+ </div>
408
+ `;
409
+ }
410
+
411
+ html += ` </div>\n`;
412
+ }
413
+
414
+ html += ` </div>
415
+ <div class="footer"><p>${t.footer}</p></div>
416
+ <script>
417
+ document.querySelectorAll('.lang-tab').forEach(tab => {
418
+ tab.addEventListener('click', () => {
419
+ const lang = tab.dataset.lang;
420
+ document.querySelectorAll('.lang-tab').forEach(t => t.classList.remove('active'));
421
+ tab.classList.add('active');
422
+ document.querySelectorAll('.language-content').forEach(c => c.classList.remove('active'));
423
+ document.getElementById('content-' + lang).classList.add('active');
424
+ });
425
+ });
426
+ document.getElementById('searchInput').addEventListener('input', function() {
427
+ const query = this.value.toLowerCase().trim();
428
+ const activeContent = document.querySelector('.language-content.active');
429
+ activeContent.querySelectorAll('.category-section').forEach(section => {
430
+ let visible = 0;
431
+ section.querySelectorAll('.template-card').forEach(card => {
432
+ const matches = card.dataset.name.includes(query) || card.dataset.path.includes(query);
433
+ card.classList.toggle('hidden', !matches);
434
+ if (matches) visible++;
435
+ });
436
+ section.classList.toggle('hidden', visible === 0);
437
+ });
438
+ });
439
+ </script>
440
+ </body>
441
+ </html>`;
442
+
443
+ fs.writeFileSync(path.join(examplesDir, 'index.html'), html);
444
+ }
445
+
446
+ /**
447
+ * Generate examples
448
+ * @param {Object} options
449
+ * @param {string} options.distDir - Directory with built HTML templates
450
+ * @param {string} options.examplesDir - Output directory for examples
451
+ * @param {string} options.configDir - Directory with example data configs
452
+ * @param {string[]} [options.languages] - Languages to process
453
+ * @param {boolean} [options.clean] - Clean before generating
454
+ * @param {boolean} [options.silent] - Suppress output
455
+ */
456
+ async function generateExamples(options = {}) {
457
+ const {
458
+ distDir,
459
+ examplesDir,
460
+ configDir,
461
+ languages = ['en', 'fr'],
462
+ clean = false,
463
+ silent = false
464
+ } = options;
465
+
466
+ if (!silent) {
467
+ console.log('\n' + '='.repeat(60));
468
+ console.log('Email Template Example Generator');
469
+ console.log('='.repeat(60));
470
+ }
471
+
472
+ // Load images config
473
+ let imagesConfig = {};
474
+ const imagesConfigPath = path.join(configDir, 'images.json');
475
+ if (fs.existsSync(imagesConfigPath)) {
476
+ imagesConfig = JSON.parse(fs.readFileSync(imagesConfigPath, 'utf-8'));
477
+ }
478
+
479
+ // Clean if requested
480
+ if (clean && fs.existsSync(examplesDir)) {
481
+ if (!silent) console.log('\n๐Ÿงน Cleaning examples directory...');
482
+ for (const lang of languages) {
483
+ const langDir = path.join(examplesDir, lang);
484
+ if (fs.existsSync(langDir)) {
485
+ await fse.remove(langDir);
486
+ }
487
+ }
488
+ const indexPath = path.join(examplesDir, 'index.html');
489
+ if (fs.existsSync(indexPath)) {
490
+ fs.unlinkSync(indexPath);
491
+ }
492
+ }
493
+
494
+ await fse.ensureDir(examplesDir);
495
+
496
+ let totalSuccess = 0;
497
+ let totalErrors = 0;
498
+ const templatesByLang = {};
499
+
500
+ for (const lang of languages) {
501
+ const sourceDir = path.join(distDir, 'i18n', lang);
502
+ const outputDir = path.join(examplesDir, lang);
503
+ const dataFile = path.join(configDir, `${lang}.json`);
504
+
505
+ // Load example data
506
+ let exampleData = {};
507
+ if (fs.existsSync(dataFile)) {
508
+ exampleData = JSON.parse(fs.readFileSync(dataFile, 'utf-8'));
509
+ } else {
510
+ if (!silent) console.log(`โš ๏ธ No data file for ${lang}: ${dataFile}`);
511
+ continue;
512
+ }
513
+
514
+ const result = await processLanguage({
515
+ lang,
516
+ sourceDir,
517
+ outputDir,
518
+ exampleData,
519
+ imagesConfig,
520
+ silent
521
+ });
522
+
523
+ totalSuccess += result.success;
524
+ totalErrors += result.errors;
525
+
526
+ // Collect template info for index
527
+ if (fs.existsSync(outputDir)) {
528
+ const htmlFiles = glob.sync('**/*.html', { cwd: outputDir });
529
+ const categories = {};
530
+ for (const file of htmlFiles) {
531
+ const parts = file.split(/[/\\]/);
532
+ const category = parts.length > 1 ? parts[0] : 'other';
533
+ if (!categories[category]) categories[category] = [];
534
+ categories[category].push(file);
535
+ }
536
+ templatesByLang[lang] = { files: htmlFiles, categories };
537
+ }
538
+ }
539
+
540
+ // Create index page
541
+ createIndex(examplesDir, languages, templatesByLang);
542
+ if (!silent) console.log(`\n๐Ÿ“„ Created examples/index.html`);
543
+
544
+ if (!silent) {
545
+ console.log('\n' + '='.repeat(60));
546
+ console.log('Summary:');
547
+ console.log('='.repeat(60));
548
+ console.log(` Languages processed: ${languages.join(', ').toUpperCase()}`);
549
+ console.log(` Templates generated: ${totalSuccess}`);
550
+ if (totalErrors > 0) console.log(` Errors: ${totalErrors}`);
551
+ console.log(`\n๐Ÿ“‚ Output: ${examplesDir}\n`);
552
+ }
553
+
554
+ return { success: totalSuccess, errors: totalErrors };
555
+ }
556
+
557
+ module.exports = {
558
+ generateExamples,
559
+ processLanguage,
560
+ createIndex,
561
+ replaceVariables,
562
+ uiTranslations
563
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-email-templates-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "CLI tools for building, translating and exporting professional MJML email templates to Mailchimp, Klaviyo, and HubSpot",
5
5
  "author": "Atsรฉ Fabrice Igor KOMAN <your.email@example.com>",
6
6
  "license": "MIT",
@@ -8,7 +8,8 @@
8
8
  "bin": {
9
9
  "email-build": "./bin/build.js",
10
10
  "email-i18n": "./bin/build-i18n.js",
11
- "email-export": "./bin/export.js"
11
+ "email-export": "./bin/export.js",
12
+ "email-examples": "./bin/examples.js"
12
13
  },
13
14
  "files": [
14
15
  "bin/",