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 +16 -0
- package/bin/examples.js +89 -0
- package/index.js +3 -1
- package/lib/examples.js +563 -0
- package/package.json +3 -2
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
|
package/bin/examples.js
ADDED
|
@@ -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
|
|
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,
|
package/lib/examples.js
ADDED
|
@@ -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.
|
|
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/",
|