domma-cms 0.3.0 → 0.5.2
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 +3 -3
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +2313 -0
- package/admin/dist/domma/domma-tools.min.js +10 -0
- package/admin/index.html +4 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +8 -4
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +18 -10
- package/admin/js/templates/action-editor.html +171 -0
- package/admin/js/templates/actions-list.html +19 -0
- package/admin/js/templates/api-reference.html +1411 -0
- package/admin/js/templates/block-editor.html +158 -0
- package/admin/js/templates/blocks.html +8 -0
- package/admin/js/templates/collection-editor.html +47 -0
- package/admin/js/templates/collection-entries.html +3 -0
- package/admin/js/templates/collections.html +51 -4
- package/admin/js/templates/documentation.html +258 -0
- package/{plugins/form-builder/admin → admin/js}/templates/form-editor.html +238 -199
- package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
- package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
- package/admin/js/templates/login.html +29 -4
- package/admin/js/templates/my-profile.html +17 -0
- package/admin/js/templates/page-editor.html +39 -0
- package/admin/js/templates/pages.html +6 -1
- package/admin/js/templates/pro-docs.html +259 -0
- package/admin/js/templates/role-editor.html +59 -0
- package/admin/js/templates/roles.html +10 -0
- package/admin/js/templates/settings.html +167 -23
- package/admin/js/templates/tutorials.html +81 -0
- package/admin/js/templates/user-editor.html +7 -0
- package/admin/js/templates/users.html +3 -26
- package/admin/js/templates/view-editor.html +201 -0
- package/admin/js/templates/view-preview.html +51 -0
- package/admin/js/templates/views-list.html +19 -0
- package/admin/js/views/action-editor.js +1 -0
- package/admin/js/views/actions-list.js +1 -0
- package/admin/js/views/api-reference.js +1 -0
- package/admin/js/views/block-editor.js +8 -0
- package/admin/js/views/blocks.js +4 -0
- package/admin/js/views/collection-editor.js +3 -3
- package/admin/js/views/collection-entries.js +1 -1
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/dashboard.js +1 -1
- package/admin/js/views/form-editor.js +8 -0
- package/admin/js/views/form-submissions.js +1 -0
- package/admin/js/views/forms.js +1 -0
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/media.js +1 -1
- package/admin/js/views/my-profile.js +1 -0
- package/admin/js/views/page-editor.js +34 -15
- package/admin/js/views/pages.js +5 -5
- package/admin/js/views/plugins.js +10 -10
- package/admin/js/views/pro-docs.js +1 -0
- package/admin/js/views/role-editor.js +1 -0
- package/admin/js/views/roles.js +4 -0
- package/admin/js/views/settings.js +3 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +4 -7
- package/admin/js/views/view-editor.js +1 -0
- package/admin/js/views/view-preview.js +1 -0
- package/admin/js/views/views-list.js +1 -0
- package/bin/cli.js +1 -1
- package/config/auth.json +1 -0
- package/config/connections.json.bak +9 -0
- package/config/connections.json.example +9 -0
- package/config/navigation.json +5 -15
- package/config/plugins.json +19 -29
- package/config/server.json +6 -6
- package/config/site.json +16 -6
- package/package.json +25 -10
- package/plugins/example-analytics/stats.json +17 -12
- package/plugins/form-builder/data/forms/contacts.json +62 -62
- package/plugins/form-builder/data/forms/enquiries.json +103 -0
- package/plugins/form-builder/data/forms/feedback.json +17 -16
- package/plugins/form-builder/data/forms/notes.json +79 -0
- package/plugins/form-builder/data/forms/to-do.json +100 -0
- package/plugins/form-builder/data/submissions/contacts.json +1 -26
- package/plugins/form-builder/data/submissions/notes.json +1 -0
- package/plugins/form-builder/data/submissions/to-do.json +1 -0
- package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
- package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
- package/plugins/theme-roller/config.js +1 -0
- package/plugins/theme-roller/plugin.js +233 -0
- package/plugins/theme-roller/plugin.json +31 -0
- package/plugins/theme-roller/public/active-theme.css +0 -0
- package/plugins/theme-roller/public/inject-head-late.html +1 -0
- package/public/css/forms.css +1 -0
- package/public/css/site.css +1 -1
- package/public/js/forms.js +1 -0
- package/public/js/site.js +1 -1
- package/scripts/build.js +194 -129
- package/scripts/pro.js +254 -0
- package/scripts/reset.js +33 -8
- package/scripts/seed.js +677 -128
- package/scripts/setup.js +1 -0
- package/server/middleware/auth.js +136 -120
- package/server/routes/api/actions.js +200 -0
- package/server/routes/api/auth.js +292 -146
- package/server/routes/api/blocks.js +84 -0
- package/server/routes/api/collections.js +79 -27
- package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +491 -505
- package/server/routes/api/layouts.js +49 -39
- package/server/routes/api/media.js +118 -92
- package/server/routes/api/navigation.js +40 -36
- package/server/routes/api/pages.js +132 -118
- package/server/routes/api/plugins.js +6 -3
- package/server/routes/api/settings.js +104 -88
- package/server/routes/api/users.js +27 -19
- package/server/routes/api/views.js +148 -0
- package/server/routes/public.js +124 -108
- package/server/server.js +269 -181
- package/server/services/actions.js +387 -0
- package/server/services/adapterRegistry.js +98 -0
- package/server/services/adapters/FileAdapter.js +192 -0
- package/server/services/adapters/MongoAdapter.js +220 -0
- package/server/services/blocks.js +162 -0
- package/server/services/collections.js +74 -86
- package/server/services/connectionManager.js +102 -0
- package/server/services/content.js +312 -307
- package/server/services/email.js +126 -0
- package/server/services/forms.js +173 -0
- package/server/services/markdown.js +1378 -747
- package/server/services/permissionRegistry.js +173 -0
- package/server/services/presetCollections.js +251 -0
- package/server/services/renderer.js +98 -2
- package/server/services/roles.js +227 -0
- package/server/services/rowAccess.js +104 -0
- package/server/services/userProfiles.js +199 -0
- package/server/services/users.js +281 -212
- package/server/services/views.js +280 -0
- package/server/templates/page.html +124 -113
- package/plugins/form-builder/admin/templates/form-settings.html +0 -29
- package/plugins/form-builder/admin/views/form-editor.js +0 -1444
- package/plugins/form-builder/admin/views/form-settings.js +0 -38
- package/plugins/form-builder/admin/views/form-submissions.js +0 -295
- package/plugins/form-builder/admin/views/forms-list.js +0 -164
- package/plugins/form-builder/config.js +0 -9
- package/plugins/form-builder/data/forms/consent.json +0 -104
- package/plugins/form-builder/data/forms/contact-details.json +0 -99
- package/plugins/form-builder/data/submissions/consent.json +0 -13
- package/plugins/form-builder/plugin.json +0 -52
- package/plugins/form-builder/public/inject-body.html +0 -352
- package/plugins/form-builder/public/inject-head.html +0 -58
- package/plugins/form-builder/public/package.json +0 -1
- package/scripts/copy-domma.js +0 -48
- package/server/services/userTypes.js +0 -167
- /package/plugins/form-builder/data/submissions/{contact-details.json → enquiries.json} +0 -0
- /package/{plugins/form-builder/public → public/js}/form-logic-engine.js +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MongoAdapter — MongoDB storage adapter for Collection entries. (Pro feature)
|
|
3
|
+
*
|
|
4
|
+
* Each CMS collection maps to a MongoDB collection prefixed with `cms_`
|
|
5
|
+
* (e.g. slug "contacts" → MongoDB collection "cms_contacts").
|
|
6
|
+
*
|
|
7
|
+
* Entry format is preserved: { id, data, meta } — identical to FileAdapter output.
|
|
8
|
+
* MongoDB's _id field is stripped from all returned documents.
|
|
9
|
+
*
|
|
10
|
+
* Adapter interface:
|
|
11
|
+
* list(slug, opts) → { entries, total, page, limit }
|
|
12
|
+
* get(slug, entryId) → entry | null
|
|
13
|
+
* insert(slug, entry) → entry
|
|
14
|
+
* update(slug, entryId, e) → entry
|
|
15
|
+
* remove(slug, entryId) → void
|
|
16
|
+
* clear(slug) → void
|
|
17
|
+
* all(slug) → entry[]
|
|
18
|
+
* insertMany(slug, entries) → { imported, skipped, errors }
|
|
19
|
+
* count(slug) → number
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Prefix applied to all CMS collection names in MongoDB to avoid collisions. */
|
|
23
|
+
const PREFIX = 'cms_';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Strip MongoDB's internal _id from a document before returning.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} doc
|
|
29
|
+
* @returns {object}
|
|
30
|
+
*/
|
|
31
|
+
function strip(doc) {
|
|
32
|
+
if (!doc) return null;
|
|
33
|
+
const { _id, ...rest } = doc;
|
|
34
|
+
return rest;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class MongoAdapter {
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {import('mongodb').Db} db - The Db instance from connectionManager
|
|
41
|
+
*/
|
|
42
|
+
constructor(db) {
|
|
43
|
+
this._db = db;
|
|
44
|
+
this._ensured = new Set();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get (and cache) the MongoDB collection for a slug, creating the unique
|
|
49
|
+
* index on `id` the first time it is accessed.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} slug
|
|
52
|
+
* @returns {Promise<import('mongodb').Collection>}
|
|
53
|
+
*/
|
|
54
|
+
async _col(slug) {
|
|
55
|
+
const col = this._db.collection(`${PREFIX}${slug}`);
|
|
56
|
+
if (!this._ensured.has(slug)) {
|
|
57
|
+
await col.createIndex({ id: 1 }, { unique: true, sparse: false });
|
|
58
|
+
this._ensured.add(slug);
|
|
59
|
+
}
|
|
60
|
+
return col;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* List entries with optional pagination, sorting, and search.
|
|
65
|
+
*
|
|
66
|
+
* When no search term is provided, pagination and sorting are pushed to MongoDB
|
|
67
|
+
* for efficiency. When a search term is provided, all documents are fetched and
|
|
68
|
+
* filtered in memory — identical behaviour to FileAdapter. For large collections
|
|
69
|
+
* requiring efficient full-text search, configure a MongoDB Atlas Search index.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} slug
|
|
72
|
+
* @param {object} [opts]
|
|
73
|
+
* @param {number} [opts.page=1]
|
|
74
|
+
* @param {number} [opts.limit=50]
|
|
75
|
+
* @param {string} [opts.sort='createdAt']
|
|
76
|
+
* @param {string} [opts.order='desc']
|
|
77
|
+
* @param {string} [opts.search]
|
|
78
|
+
* @returns {Promise<{ entries: object[], total: number, page: number, limit: number }>}
|
|
79
|
+
*/
|
|
80
|
+
async list(slug, { page = 1, limit = 50, sort = 'createdAt', order = 'desc', search } = {}) {
|
|
81
|
+
const col = await this._col(slug);
|
|
82
|
+
|
|
83
|
+
const sortField = sort === 'createdAt' ? 'meta.createdAt' : `data.${sort}`;
|
|
84
|
+
const sortDir = order === 'desc' ? -1 : 1;
|
|
85
|
+
|
|
86
|
+
if (search) {
|
|
87
|
+
// Fetch all and filter in memory to avoid $where (often disabled in Atlas).
|
|
88
|
+
const all = await col
|
|
89
|
+
.find({}, { projection: { _id: 0 } })
|
|
90
|
+
.sort({ [sortField]: sortDir })
|
|
91
|
+
.toArray();
|
|
92
|
+
|
|
93
|
+
const term = search.toLowerCase();
|
|
94
|
+
const matched = all.filter(entry =>
|
|
95
|
+
Object.values(entry.data || {}).some(v => String(v).toLowerCase().includes(term))
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const total = matched.length;
|
|
99
|
+
const offset = (page - 1) * limit;
|
|
100
|
+
return { entries: matched.slice(offset, offset + limit), total, page, limit };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const total = await col.countDocuments({});
|
|
104
|
+
const offset = (page - 1) * limit;
|
|
105
|
+
|
|
106
|
+
const docs = await col
|
|
107
|
+
.find({}, { projection: { _id: 0 } })
|
|
108
|
+
.sort({ [sortField]: sortDir })
|
|
109
|
+
.skip(offset)
|
|
110
|
+
.limit(limit)
|
|
111
|
+
.toArray();
|
|
112
|
+
|
|
113
|
+
return { entries: docs, total, page, limit };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get a single entry by CMS entry ID.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} slug
|
|
120
|
+
* @param {string} entryId
|
|
121
|
+
* @returns {Promise<object|null>}
|
|
122
|
+
*/
|
|
123
|
+
async get(slug, entryId) {
|
|
124
|
+
const col = await this._col(slug);
|
|
125
|
+
const doc = await col.findOne({ id: entryId }, { projection: { _id: 0 } });
|
|
126
|
+
return doc || null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Insert a pre-built entry (with id and meta already set by collections.js).
|
|
131
|
+
*
|
|
132
|
+
* @param {string} slug
|
|
133
|
+
* @param {object} entry
|
|
134
|
+
* @returns {Promise<object>}
|
|
135
|
+
*/
|
|
136
|
+
async insert(slug, entry) {
|
|
137
|
+
const col = await this._col(slug);
|
|
138
|
+
await col.insertOne({ ...entry });
|
|
139
|
+
return entry;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Replace an existing entry's data and meta.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} slug
|
|
146
|
+
* @param {string} entryId
|
|
147
|
+
* @param {object} updated - Full updated entry object
|
|
148
|
+
* @returns {Promise<object>}
|
|
149
|
+
* @throws {Error} If entry not found
|
|
150
|
+
*/
|
|
151
|
+
async update(slug, entryId, updated) {
|
|
152
|
+
const col = await this._col(slug);
|
|
153
|
+
const result = await col.replaceOne({ id: entryId }, { ...updated });
|
|
154
|
+
if (result.matchedCount === 0) throw new Error('Entry not found');
|
|
155
|
+
return updated;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Delete a single entry by CMS entry ID.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} slug
|
|
162
|
+
* @param {string} entryId
|
|
163
|
+
* @returns {Promise<void>}
|
|
164
|
+
* @throws {Error} If entry not found
|
|
165
|
+
*/
|
|
166
|
+
async remove(slug, entryId) {
|
|
167
|
+
const col = await this._col(slug);
|
|
168
|
+
const result = await col.deleteOne({ id: entryId });
|
|
169
|
+
if (result.deletedCount === 0) throw new Error('Entry not found');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Delete all entries in a collection.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} slug
|
|
176
|
+
* @returns {Promise<void>}
|
|
177
|
+
*/
|
|
178
|
+
async clear(slug) {
|
|
179
|
+
const col = await this._col(slug);
|
|
180
|
+
await col.deleteMany({});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Return all entries (used for export).
|
|
185
|
+
*
|
|
186
|
+
* @param {string} slug
|
|
187
|
+
* @returns {Promise<object[]>}
|
|
188
|
+
*/
|
|
189
|
+
async all(slug) {
|
|
190
|
+
const col = await this._col(slug);
|
|
191
|
+
return col.find({}, { projection: { _id: 0 } }).toArray();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Bulk-insert pre-built entries (already validated by collections.js).
|
|
196
|
+
*
|
|
197
|
+
* @param {string} slug
|
|
198
|
+
* @param {object[]} entries
|
|
199
|
+
* @returns {Promise<{ imported: number, skipped: number, errors: string[] }>}
|
|
200
|
+
*/
|
|
201
|
+
async insertMany(slug, entries) {
|
|
202
|
+
if (entries.length === 0) return { imported: 0, skipped: 0, errors: [] };
|
|
203
|
+
const col = await this._col(slug);
|
|
204
|
+
await col.insertMany(entries.map(e => ({ ...e })));
|
|
205
|
+
return { imported: entries.length, skipped: 0, errors: [] };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Count total entries in a collection.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} slug
|
|
212
|
+
* @returns {Promise<number>}
|
|
213
|
+
*/
|
|
214
|
+
async count(slug) {
|
|
215
|
+
const col = await this._col(slug);
|
|
216
|
+
return col.countDocuments({});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export default MongoAdapter;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blocks Service
|
|
3
|
+
* Seeds default block templates for built-in forms.
|
|
4
|
+
* Never overwrites existing files — user customisations are preserved.
|
|
5
|
+
*/
|
|
6
|
+
import {access, writeFile} from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import {fileURLToPath} from 'url';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const BLOCKS_DIR = path.resolve(__dirname, '../../content/blocks');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Write a block file only if it does not already exist.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} name - Block name (without .html extension)
|
|
17
|
+
* @param {string} content - HTML template content
|
|
18
|
+
*/
|
|
19
|
+
async function seedBlock(name, content) {
|
|
20
|
+
const filePath = path.join(BLOCKS_DIR, `${name}.html`);
|
|
21
|
+
try {
|
|
22
|
+
await access(filePath);
|
|
23
|
+
// File exists — leave it alone
|
|
24
|
+
} catch {
|
|
25
|
+
await writeFile(filePath, content.trim() + '\n', 'utf8');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Default block templates
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const BLOCKS = {
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* contact-card — for the Contacts form
|
|
37
|
+
* Fields: full_name, phone_number, email_address
|
|
38
|
+
*/
|
|
39
|
+
'contact-card': `
|
|
40
|
+
<div class="card mb-3">
|
|
41
|
+
<div class="card-body">
|
|
42
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;flex-wrap:wrap;">
|
|
43
|
+
<div>
|
|
44
|
+
<h4 style="margin:0 0 .5rem;font-size:1.05rem;">{{full_name}}</h4>
|
|
45
|
+
<div style="display:flex;flex-direction:column;gap:.3rem;font-size:.875rem;">
|
|
46
|
+
<span><strong>Email:</strong> {{email_address}}</span>
|
|
47
|
+
<span><strong>Phone:</strong> {{phone_number}}</span>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<small class="text-muted" style="white-space:nowrap;">{{_createdAt}}</small>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
`,
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* enquiry-card — for the Enquiries form
|
|
58
|
+
* Fields: full_name, email, phone, subject, message
|
|
59
|
+
*/
|
|
60
|
+
'enquiry-card': `
|
|
61
|
+
<div class="card mb-3">
|
|
62
|
+
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;gap:.75rem;flex-wrap:wrap;">
|
|
63
|
+
<div>
|
|
64
|
+
<strong style="font-size:1rem;">{{subject}}</strong>
|
|
65
|
+
<span class="text-muted" style="font-size:.8rem;margin-left:.6rem;">from {{full_name}}</span>
|
|
66
|
+
</div>
|
|
67
|
+
<small class="text-muted">{{_createdAt}}</small>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="card-body">
|
|
70
|
+
<p style="margin:0 0 1rem;line-height:1.65;">{{message}}</p>
|
|
71
|
+
<div style="display:flex;gap:1.5rem;font-size:.8rem;color:var(--dm-text-muted,#888);flex-wrap:wrap;">
|
|
72
|
+
<span><strong>Email:</strong> {{email}}</span>
|
|
73
|
+
<span><strong>Phone:</strong> {{phone}}</span>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
`,
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* feedback-card — for the Feedback form
|
|
81
|
+
* Fields: name, email, rating, category, subject, message, recommend
|
|
82
|
+
*/
|
|
83
|
+
'feedback-card': `
|
|
84
|
+
<div class="card mb-3">
|
|
85
|
+
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;gap:.75rem;flex-wrap:wrap;">
|
|
86
|
+
<div style="display:flex;align-items:center;gap:.65rem;flex-wrap:wrap;">
|
|
87
|
+
<strong>{{subject}}</strong>
|
|
88
|
+
<span class="badge badge-secondary" style="text-transform:capitalize;">{{category}}</span>
|
|
89
|
+
<span class="badge badge-info" style="text-transform:capitalize;">{{rating}}</span>
|
|
90
|
+
</div>
|
|
91
|
+
<small class="text-muted">{{_createdAt}}</small>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="card-body">
|
|
94
|
+
<p style="margin:0 0 1rem;line-height:1.65;">{{message}}</p>
|
|
95
|
+
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.5rem;font-size:.8rem;color:var(--dm-text-muted,#888);">
|
|
96
|
+
<span>{{name}} — {{email}}</span>
|
|
97
|
+
<span>Would recommend: <strong>{{recommend}}</strong></span>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
`,
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* note-card — for the Notes form
|
|
105
|
+
* Fields: title, content, category, tags
|
|
106
|
+
*/
|
|
107
|
+
'note-card': `
|
|
108
|
+
<div class="card mb-3">
|
|
109
|
+
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;gap:.75rem;flex-wrap:wrap;">
|
|
110
|
+
<div style="display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;">
|
|
111
|
+
<strong style="font-size:1rem;">{{title}}</strong>
|
|
112
|
+
<span class="badge badge-secondary" style="text-transform:capitalize;">{{category}}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<small class="text-muted">{{_createdAt}}</small>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="card-body">
|
|
117
|
+
<p style="margin:0 0 .75rem;line-height:1.7;white-space:pre-line;">{{content}}</p>
|
|
118
|
+
<small class="text-muted">Tags: {{tags}}</small>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
`,
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* todo-item — for the To-Do form
|
|
125
|
+
* Fields: title, description, status, priority, due_date, assigned_to
|
|
126
|
+
*/
|
|
127
|
+
'todo-item': `
|
|
128
|
+
<div class="card mb-2">
|
|
129
|
+
<div class="card-body" style="display:flex;align-items:flex-start;gap:1rem;flex-wrap:wrap;">
|
|
130
|
+
<div style="flex:1;min-width:0;">
|
|
131
|
+
<div style="display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;margin-bottom:.4rem;">
|
|
132
|
+
<strong style="font-size:.975rem;">{{title}}</strong>
|
|
133
|
+
<span class="badge badge-warning" style="text-transform:capitalize;">{{priority}}</span>
|
|
134
|
+
<span class="badge badge-success" style="text-transform:capitalize;">{{status}}</span>
|
|
135
|
+
</div>
|
|
136
|
+
<p style="margin:0 0 .5rem;font-size:.875rem;line-height:1.55;color:var(--dm-text-muted,#888);">{{description}}</p>
|
|
137
|
+
<div style="display:flex;gap:1.5rem;font-size:.8rem;color:var(--dm-text-muted,#888);flex-wrap:wrap;">
|
|
138
|
+
<span><strong>Due:</strong> {{due_date}}</span>
|
|
139
|
+
<span><strong>Assigned to:</strong> {{assigned_to}}</span>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
`,
|
|
145
|
+
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Public API
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Seed all default block templates into content/blocks/.
|
|
154
|
+
* Skips any file that already exists to preserve user customisations.
|
|
155
|
+
*/
|
|
156
|
+
export async function seedDefaultBlocks() {
|
|
157
|
+
for (const [name, content] of Object.entries(BLOCKS)) {
|
|
158
|
+
await seedBlock(name, content);
|
|
159
|
+
}
|
|
160
|
+
const names = Object.keys(BLOCKS).join(', ');
|
|
161
|
+
console.log(`[blocks] Seeded default blocks: ${names}`);
|
|
162
|
+
}
|