domma-cms 0.2.0 → 0.3.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.
Files changed (72) hide show
  1. package/README.md +2 -3
  2. package/admin/css/admin.css +1 -1200
  3. package/admin/js/api.js +1 -242
  4. package/admin/js/app.js +5 -279
  5. package/admin/js/config/sidebar-config.js +1 -115
  6. package/admin/js/lib/card.js +1 -63
  7. package/admin/js/lib/image-editor.js +1 -869
  8. package/admin/js/lib/markdown-toolbar.js +46 -421
  9. package/admin/js/templates/layouts.html +44 -7
  10. package/admin/js/templates/page-editor.html +9 -0
  11. package/admin/js/templates/settings.html +18 -1
  12. package/admin/js/templates/users.html +29 -4
  13. package/admin/js/views/collection-editor.js +3 -487
  14. package/admin/js/views/collection-entries.js +1 -484
  15. package/admin/js/views/collections.js +1 -153
  16. package/admin/js/views/dashboard.js +1 -56
  17. package/admin/js/views/documentation.js +1 -12
  18. package/admin/js/views/index.js +1 -39
  19. package/admin/js/views/layouts.js +9 -42
  20. package/admin/js/views/login.js +7 -251
  21. package/admin/js/views/media.js +1 -240
  22. package/admin/js/views/navigation.js +14 -212
  23. package/admin/js/views/page-editor.js +53 -661
  24. package/admin/js/views/pages.js +5 -72
  25. package/admin/js/views/plugins.js +13 -90
  26. package/admin/js/views/settings.js +1 -199
  27. package/admin/js/views/tutorials.js +1 -12
  28. package/admin/js/views/user-editor.js +1 -88
  29. package/admin/js/views/users.js +7 -76
  30. package/bin/cli.js +18 -9
  31. package/config/auth.json +1 -17
  32. package/config/navigation.json +15 -0
  33. package/config/site.json +5 -4
  34. package/package.json +1 -1
  35. package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
  36. package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
  37. package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
  38. package/plugins/domma-effects/public/celebrations/index.js +1 -535
  39. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
  40. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
  41. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
  42. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
  43. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
  44. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
  45. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
  46. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
  47. package/plugins/example-analytics/stats.json +16 -12
  48. package/plugins/form-builder/admin/templates/form-editor.html +158 -130
  49. package/plugins/form-builder/admin/views/form-editor.js +3 -1
  50. package/plugins/form-builder/data/forms/contact-details.json +71 -35
  51. package/plugins/form-builder/data/forms/feedback.json +130 -0
  52. package/plugins/form-builder/data/submissions/feedback.json +1 -0
  53. package/plugins/form-builder/public/form-logic-engine.js +1 -568
  54. package/public/css/site.css +1 -302
  55. package/public/js/btt.js +1 -90
  56. package/public/js/cookie-consent.js +1 -61
  57. package/public/js/site.js +1 -204
  58. package/scripts/setup.js +12 -9
  59. package/server/middleware/auth.js +44 -21
  60. package/server/routes/api/auth.js +38 -8
  61. package/server/routes/api/collections.js +18 -5
  62. package/server/routes/api/layouts.js +18 -4
  63. package/server/routes/api/media.js +2 -3
  64. package/server/routes/api/navigation.js +2 -3
  65. package/server/routes/api/pages.js +3 -3
  66. package/server/routes/api/settings.js +2 -3
  67. package/server/routes/api/users.js +4 -6
  68. package/server/routes/public.js +3 -3
  69. package/server/server.js +8 -0
  70. package/server/services/markdown.js +102 -3
  71. package/server/services/userTypes.js +167 -0
  72. package/plugins/form-builder/email.js +0 -103
package/admin/js/api.js CHANGED
@@ -1,242 +1 @@
1
- /**
2
- * Admin API Client
3
- * Authenticated requests using Bearer tokens with automatic refresh on 401.
4
- * All existing api.* namespaces are preserved — views need no changes.
5
- */
6
-
7
- const BASE = '/api';
8
-
9
- // ---------------------------------------------------------------------------
10
- // Token helpers
11
- // ---------------------------------------------------------------------------
12
-
13
- function getToken() { return S.get('auth_token'); }
14
- function getRefreshToken() { return S.get('auth_refresh_token'); }
15
- function setToken(t) { S.set('auth_token', t); }
16
- function clearAuth() {
17
- S.remove('auth_token');
18
- S.remove('auth_refresh_token');
19
- S.remove('auth_user');
20
- }
21
-
22
- /**
23
- * Attempt to refresh the access token using the stored refresh token.
24
- * Clears auth and redirects to login if refresh fails.
25
- *
26
- * @returns {Promise<string>} New access token
27
- */
28
- async function refreshAccessToken() {
29
- const refreshToken = getRefreshToken();
30
- if (!refreshToken) throw new Error('No refresh token');
31
-
32
- const res = await fetch(`${BASE}/auth/refresh`, {
33
- method: 'POST',
34
- headers: { 'Content-Type': 'application/json' },
35
- body: JSON.stringify({ refreshToken })
36
- });
37
-
38
- if (!res.ok) {
39
- clearAuth();
40
- R.navigate('/login');
41
- throw new Error('Token refresh failed');
42
- }
43
-
44
- const { token } = await res.json();
45
- setToken(token);
46
- return token;
47
- }
48
-
49
- // ---------------------------------------------------------------------------
50
- // Core request function
51
- // ---------------------------------------------------------------------------
52
-
53
- /**
54
- * Make an authenticated API request.
55
- * Automatically retries once after refreshing the access token on 401.
56
- *
57
- * @param {string} endpoint
58
- * @param {RequestInit} options
59
- * @returns {Promise<any>}
60
- */
61
- async function apiRequest(endpoint, options = {}) {
62
- let token = getToken();
63
-
64
- const buildHeaders = (t) => ({
65
- ...(options.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
66
- ...options.headers,
67
- ...(t ? { Authorization: `Bearer ${t}` } : {})
68
- });
69
-
70
- let res = await fetch(`${BASE}${endpoint}`, { ...options, headers: buildHeaders(token) });
71
-
72
- // On 401, try refreshing once
73
- if (res.status === 401 && getRefreshToken()) {
74
- try {
75
- token = await refreshAccessToken();
76
- res = await fetch(`${BASE}${endpoint}`, { ...options, headers: buildHeaders(token) });
77
- } catch {
78
- return; // Already redirected to login
79
- }
80
- }
81
-
82
- if (!res.ok) {
83
- const body = await res.json().catch(() => ({ error: 'Request failed' }));
84
- throw new Error(body.error || body.message || `HTTP ${res.status}`);
85
- }
86
-
87
- // 204 No Content
88
- if (res.status === 204) return null;
89
-
90
- return res.json();
91
- }
92
-
93
- /**
94
- * Upload a file with Bearer token (multipart — no Content-Type override).
95
- *
96
- * @param {string} endpoint
97
- * @param {FormData} formData
98
- * @returns {Promise<any>}
99
- */
100
- async function uploadRequest(endpoint, formData) {
101
- const token = getToken();
102
- const headers = token ? { Authorization: `Bearer ${token}` } : {};
103
-
104
- const res = await fetch(`${BASE}${endpoint}`, { method: 'POST', headers, body: formData });
105
- if (!res.ok) {
106
- const body = await res.json().catch(() => ({ error: 'Upload failed' }));
107
- throw new Error(body.error || body.message || `HTTP ${res.status}`);
108
- }
109
- return res.json();
110
- }
111
-
112
- // ---------------------------------------------------------------------------
113
- // Domain API namespaces
114
- // ---------------------------------------------------------------------------
115
-
116
- export const api = {
117
- // Auth
118
- auth: {
119
- setupStatus: () => apiRequest('/auth/setup-status', { method: 'GET' }),
120
- setup: (data) => apiRequest('/auth/setup', { method: 'POST', body: JSON.stringify(data) }),
121
- login: (data) => apiRequest('/auth/login', { method: 'POST', body: JSON.stringify(data) }),
122
- me: () => apiRequest('/auth/me', { method: 'GET' }),
123
- refresh: (refreshToken) => apiRequest('/auth/refresh', { method: 'POST', body: JSON.stringify({ refreshToken }) })
124
- },
125
-
126
- // Pages
127
- pages: {
128
- list: () => apiRequest('/pages', { method: 'GET' }),
129
- get: (urlPath) => apiRequest(`/pages${urlPath}`, { method: 'GET' }),
130
- create: (payload) => apiRequest('/pages', { method: 'POST', body: JSON.stringify(payload) }),
131
- update: (urlPath, payload) => apiRequest(`/pages${urlPath}`, { method: 'PUT', body: JSON.stringify(payload) }),
132
- delete: (urlPath) => apiRequest(`/pages${urlPath}`, {method: 'DELETE'}),
133
- preview: (markdown) => apiRequest('/pages/preview', {method: 'POST', body: JSON.stringify({markdown})})
134
- },
135
-
136
- // Settings
137
- settings: {
138
- get: () => apiRequest('/settings', { method: 'GET' }),
139
- save: (data) => apiRequest('/settings', { method: 'PUT', body: JSON.stringify(data) })
140
- },
141
-
142
- // Navigation
143
- navigation: {
144
- get: () => apiRequest('/navigation', { method: 'GET' }),
145
- save: (data) => apiRequest('/navigation', { method: 'PUT', body: JSON.stringify(data) })
146
- },
147
-
148
- // Layouts (presets)
149
- layouts: {
150
- get: () => apiRequest('/layouts', { method: 'GET' }),
151
- save: (data) => apiRequest('/layouts', { method: 'PUT', body: JSON.stringify(data) })
152
- },
153
-
154
- // Media
155
- media: {
156
- list: () => apiRequest('/media', { method: 'GET' }),
157
- upload: (formData) => uploadRequest('/media', formData),
158
- delete: (name) => apiRequest(`/media/${encodeURIComponent(name)}`, {method: 'DELETE'}),
159
- rename: (name, newName) => apiRequest(`/media/${encodeURIComponent(name)}`, {
160
- method: 'PATCH',
161
- body: JSON.stringify({newName})
162
- }),
163
- info: (name) => apiRequest(`/media/${encodeURIComponent(name)}/info`, {method: 'GET'}),
164
- transform: (name, body) => apiRequest(`/media/${encodeURIComponent(name)}/transform`, {
165
- method: 'POST',
166
- body: JSON.stringify(body)
167
- }),
168
- },
169
-
170
- // Users
171
- users: {
172
- list: () => apiRequest('/users', { method: 'GET' }),
173
- get: (id) => apiRequest(`/users/${id}`, { method: 'GET' }),
174
- create: (data) => apiRequest('/users', { method: 'POST', body: JSON.stringify(data) }),
175
- update: (id, data) => apiRequest(`/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
176
- delete: (id) => apiRequest(`/users/${id}`, { method: 'DELETE' })
177
- },
178
-
179
- // Plugins
180
- plugins: {
181
- list: () => apiRequest('/plugins', { method: 'GET' }),
182
- update: (name, data) => apiRequest(`/plugins/${name}`, { method: 'PUT', body: JSON.stringify(data) }),
183
- adminConfig: () => apiRequest('/plugins/admin-config', { method: 'GET' })
184
- },
185
-
186
- // Collections
187
- collections: {
188
- list: () => apiRequest('/collections', { method: 'GET' }),
189
- get: (slug) => apiRequest(`/collections/${slug}`, { method: 'GET' }),
190
- create: (data) => apiRequest('/collections', { method: 'POST', body: JSON.stringify(data) }),
191
- update: (slug, data) => apiRequest(`/collections/${slug}`, { method: 'PUT', body: JSON.stringify(data) }),
192
- delete: (slug) => apiRequest(`/collections/${slug}`, { method: 'DELETE' }),
193
-
194
- listEntries: (slug, params = {}) => {
195
- const qs = new URLSearchParams(params).toString();
196
- return apiRequest(`/collections/${slug}/entries${qs ? '?' + qs : ''}`, { method: 'GET' });
197
- },
198
- getEntry: (slug, id) => apiRequest(`/collections/${slug}/entries/${id}`, { method: 'GET' }),
199
- createEntry: (slug, data) => apiRequest(`/collections/${slug}/entries`, { method: 'POST', body: JSON.stringify({ data }) }),
200
- updateEntry: (slug, id, data) => apiRequest(`/collections/${slug}/entries/${id}`, { method: 'PUT', body: JSON.stringify({ data }) }),
201
- deleteEntry: (slug, id) => apiRequest(`/collections/${slug}/entries/${id}`, { method: 'DELETE' }),
202
- clearEntries: (slug) => apiRequest(`/collections/${slug}/entries`, { method: 'DELETE' }),
203
-
204
- import: (slug, entries) => apiRequest(`/collections/${slug}/import`, { method: 'POST', body: JSON.stringify({ entries }) }),
205
-
206
- // Public API (no auth needed when collection is public)
207
- publicList: (slug, params = {}) => {
208
- const qs = new URLSearchParams(params).toString();
209
- return apiRequest(`/collections/${slug}/public${qs ? '?' + qs : ''}`, { method: 'GET' });
210
- }
211
- },
212
-
213
- // Settings (extended)
214
- settingsExt: {
215
- testEmail: (to) => apiRequest('/settings/test-email', { method: 'POST', body: JSON.stringify({ to }) })
216
- }
217
- };
218
-
219
- // ---------------------------------------------------------------------------
220
- // Auth state helpers (used by app.js)
221
- // ---------------------------------------------------------------------------
222
-
223
- export function isAuthenticated() {
224
- return !!getToken();
225
- }
226
-
227
- export function getUser() {
228
- return S.get('auth_user');
229
- }
230
-
231
- export function setAuthData({ token, refreshToken, user }) {
232
- if (token) setToken(token);
233
- if (refreshToken) S.set('auth_refresh_token', refreshToken);
234
- if (user) S.set('auth_user', user);
235
- }
236
-
237
- export function logout() {
238
- clearAuth();
239
- R.navigate('/login');
240
- }
241
-
242
- export {apiRequest};
1
+ const r="/api";function d(){return S.get("auth_token")}function h(){return S.get("auth_refresh_token")}function c(e){S.set("auth_token",e)}function u(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function m(){const e=h();if(!e)throw new Error("No refresh token");const o=await fetch(`${r}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw u(),R.navigate("/login"),new Error("Token refresh failed");const{token:n}=await o.json();return c(n),n}async function t(e,o={}){let n=d();const a=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let s=await fetch(`${r}${e}`,{...o,headers:a(n)});if(s.status===401&&h())try{n=await m(),s=await fetch(`${r}${e}`,{...o,headers:a(n)})}catch{return}if(!s.ok){const i=await s.json().catch(()=>({error:"Request failed"}));throw new Error(i.error||i.message||`HTTP ${s.status}`)}return s.status===204?null:s.json()}async function l(e,o){const n=d(),a=n?{Authorization:`Bearer ${n}`}:{},s=await fetch(`${r}${e}`,{method:"POST",headers:a,body:o});if(!s.ok){const i=await s.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${s.status}`)}return s.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),me:()=>t("/auth/me",{method:"GET"}),refresh:e=>t("/auth/refresh",{method:"POST",body:JSON.stringify({refreshToken:e})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})})},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},layouts:{get:()=>t("/layouts",{method:"GET"}),save:e=>t("/layouts",{method:"PUT",body:JSON.stringify(e)}),getOptions:()=>t("/layouts/options",{method:"GET"}),saveOptions:e=>t("/layouts/options",{method:"PUT",body:JSON.stringify(e)})},media:{list:()=>t("/media",{method:"GET"}),upload:e=>l("/media",e),delete:e=>t(`/media/${encodeURIComponent(e)}`,{method:"DELETE"}),rename:(e,o)=>t(`/media/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({newName:o})}),info:e=>t(`/media/${encodeURIComponent(e)}/info`,{method:"GET"}),transform:(e,o)=>t(`/media/${encodeURIComponent(e)}/transform`,{method:"POST",body:JSON.stringify(o)})},users:{list:()=>t("/users",{method:"GET"}),get:e=>t(`/users/${e}`,{method:"GET"}),create:e=>t("/users",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/users/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/users/${e}`,{method:"DELETE"})},plugins:{list:()=>t("/plugins",{method:"GET"}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"})},collections:{list:()=>t("/collections",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${n?"?"+n:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,n)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:n})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/public${n?"?"+n:""}`,{method:"GET"})}},settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})}};export function isAuthenticated(){return!!d()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:n}){e&&c(e),o&&S.set("auth_refresh_token",o),n&&S.set("auth_user",n)}export function logout(){const e=h();e&&fetch(`${r}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),u(),R.navigate("/login")}export{t as apiRequest};
package/admin/js/app.js CHANGED
@@ -1,231 +1,7 @@
1
- /**
2
- * Domma CMS Admin - SPA Entry Point
3
- * Mirrors the domma-board candidate/app.js pattern.
4
- * Adds JWT auth guard, role-aware sidebar, and dynamic plugin loading.
5
- */
6
- import {getSidebarConfig} from './config/sidebar-config.js';
7
- import {views as coreViews} from './views/index.js';
8
- import {api, getUser, isAuthenticated, logout} from './api.js';
9
-
10
- $(() => {
11
- // Fetch admin theme preference from server config; fall back to charcoal-dark
12
- (async () => {
13
- try {
14
- const site = await api.settings.get();
15
- Domma.theme.init({ theme: site.adminTheme || 'charcoal-dark', persist: true });
16
- } catch {
17
- Domma.theme.init({ theme: 'charcoal-dark', persist: true });
18
- }
19
- })();
20
-
21
- // -------------------------------------------------------------------------
22
- // Auth guard — redirect to login unless already on /login
23
- // -------------------------------------------------------------------------
24
- R.use(async (to, from, next) => {
25
- if (to.path === '/login') return next();
26
-
27
- if (!isAuthenticated()) {
28
- R.navigate('/login');
29
- return;
30
- }
31
-
32
- next();
33
- });
34
-
35
- // -------------------------------------------------------------------------
36
- // Sidebar (role-aware — re-built after login lands on dashboard)
37
- // -------------------------------------------------------------------------
38
- let sidebar = null;
39
-
40
- async function fetchSidebarCounts() {
41
- if (!isAuthenticated()) return {};
42
- try {
43
- const [pages, media, users, plugins, collections] = await Promise.all([
44
- api.pages.list().catch(() => []),
45
- api.media.list().catch(() => []),
46
- api.users.list().catch(() => []),
47
- api.plugins.list().catch(() => []),
48
- api.collections.list().catch(() => [])
49
- ]);
50
- return {
51
- pages: pages.length,
52
- media: media.length,
53
- users: users.length,
54
- plugins: plugins.filter(p => p.enabled).length || plugins.length,
55
- collections: collections.length
56
- };
57
- } catch {
58
- return {};
59
- }
60
- }
61
-
62
- async function buildSidebar(role) {
63
- if (sidebar) {
64
- $('#admin-sidebar').empty();
65
- }
66
- const counts = await fetchSidebarCounts();
67
- sidebar = Domma.elements.sidebar('#admin-sidebar', {
68
- header: { title: 'CMS Admin', icon: 'layout' },
69
- items: getSidebarConfig(role, counts, pluginSidebarItems),
70
- collapsible: true,
71
- collapseAt: 992,
72
- push: true,
73
- contentSelector: '.dashboard-main',
74
- top: '60px'
75
- });
76
- attachSidebarTooltips();
77
- attachCollapsibleHeadings();
78
- }
79
-
80
- function attachSidebarTooltips() {
81
- $('#admin-sidebar .sidebar-link').each(function () {
82
- const label = $(this).find('.sidebar-text').text().trim();
83
- if (label) $(this).attr('data-tooltip', label);
84
- });
85
- E.tooltip('#admin-sidebar [data-tooltip]', { placement: 'right' });
86
- }
87
-
88
- function attachCollapsibleHeadings() {
89
- $('#admin-sidebar .sidebar-heading').each(function () {
90
- const $heading = $(this);
91
- $heading.addClass('sidebar-heading--collapsible');
92
- $heading.append('<span class="sidebar-heading-toggle"><span data-icon="chevron-down"></span></span>');
93
-
94
- $heading.on('click', function () {
95
- const collapsing = !$heading.hasClass('is-collapsed');
96
- $heading.toggleClass('is-collapsed', collapsing);
97
-
98
- let $el = $heading.next();
99
- while ($el.length && !$el.hasClass('sidebar-heading') && !$el.hasClass('sidebar-divider')) {
100
- $el.toggle(!collapsing);
101
- $el = $el.next();
102
- }
103
- });
104
- });
105
-
106
- Domma.icons.scan('#admin-sidebar');
107
- }
108
-
109
- // Build with no role initially (login page has no sidebar)
110
- buildSidebar(null);
111
-
112
- // Keep sidebar active item in sync with route
113
- M.subscribe('router:afterChange', ({ to }) => {
114
- if (sidebar && to.path !== '/login') {
115
- sidebar.setActive('#' + to.path);
116
- }
117
- });
118
-
119
- // -------------------------------------------------------------------------
120
- // Router — core routes + login
121
- // -------------------------------------------------------------------------
122
- const coreRoutes = [
123
- { path: '/', view: 'dashboard', title: 'Dashboard - Domma CMS' },
124
- { path: '/pages', view: 'pages', title: 'Pages - Domma CMS' },
125
- { path: '/pages/new', view: 'pageEditor', title: 'New Page - Domma CMS' },
126
- { path: '/pages/edit/*', view: 'pageEditor', title: 'Edit Page - Domma CMS' },
127
- { path: '/media', view: 'media', title: 'Media - Domma CMS' },
128
- { path: '/navigation', view: 'navigation', title: 'Navigation - Domma CMS' },
129
- { path: '/layouts', view: 'layouts', title: 'Layouts - Domma CMS' },
130
- { path: '/settings', view: 'settings', title: 'Settings - Domma CMS' },
131
- { path: '/users', view: 'users', title: 'Users - Domma CMS' },
132
- { path: '/users/new', view: 'userEditor', title: 'New User - Domma CMS' },
133
- { path: '/users/edit/:id', view: 'userEditor', title: 'Edit User - Domma CMS' },
134
- {path: '/plugins', view: 'plugins', title: 'Plugins - Domma CMS'},
135
- {path: '/documentation', view: 'documentation', title: 'Usage - Domma CMS'},
136
- {path: '/tutorials', view: 'tutorials', title: 'Tutorials - Domma CMS'},
137
- {path: '/collections', view: 'collections', title: 'Collections - Domma CMS'},
138
- {path: '/collections/new', view: 'collectionEditor', title: 'New Collection - Domma CMS'},
139
- {path: '/collections/edit/:slug', view: 'collectionEditor', title: 'Edit Collection - Domma CMS'},
140
- {path: '/collections/:slug/entries',view: 'collectionEntries', title: 'Entries - Domma CMS'},
141
- { path: '/login', view: 'login', title: 'Sign in - Domma CMS',
142
- onEnter: () => {
143
- $('#admin-sidebar').hide();
144
- $('#admin-topbar').hide();
145
- }
146
- }
147
- ];
148
-
149
- // After login, re-show chrome and rebuild sidebar with correct role
150
- M.subscribe('router:afterChange', ({ to, from }) => {
151
- if (to.path !== '/login') {
152
- $('#admin-sidebar').show();
153
- $('#admin-topbar').show();
154
- if (from?.path === '/login') {
155
- const user = getUser();
156
- if (user) buildSidebar(user.role);
157
- }
158
- injectUserMenu();
159
- }
160
- });
161
-
162
- // Reveal primary/danger buttons after each view renders
163
- M.subscribe('router:afterChange', () => {
164
- setTimeout(() => {
165
- if ($('.btn-primary, .btn-danger').length) {
166
- Domma.effects.reveal('.btn-primary, .btn-danger', {animation: 'fade', stagger: 40, duration: 300});
167
- }
168
- }, 50);
169
- });
170
-
171
- // Collapsible cards — event delegation handles all views including plugins
172
- $('#view-container').on('click', '.card-collapsible .card-header', function (e) {
173
- if ($(e.target).closest('button, a').length) return;
174
- $(this).closest('.card').toggleClass('card-collapsed');
175
- });
176
-
177
- // Global Ctrl+S / Cmd+S — click the primary save button in any view
178
- document.addEventListener('keydown', (e) => {
179
- if (!(e.ctrlKey || e.metaKey) || e.key !== 's') return;
180
- if (window.location.hash === '#/login') return;
181
- const saveBtn = document.querySelector('#view-container .view-header button.btn-primary');
182
- if (saveBtn) {
183
- e.preventDefault();
184
- saveBtn.click();
185
- }
186
- });
187
-
188
- // -------------------------------------------------------------------------
189
- // Dynamic plugin routes (loaded before R.init)
190
- // -------------------------------------------------------------------------
191
- const allViews = { ...coreViews };
192
- const allRoutes = [...coreRoutes];
193
- let pluginSidebarItems = [];
194
-
195
- async function loadPlugins() {
196
- if (!isAuthenticated()) return;
197
- try {
198
- const pluginConfig = await api.plugins.adminConfig();
199
- pluginSidebarItems = pluginConfig.sidebar || [];
200
- if (pluginConfig.routes?.length) allRoutes.push(...pluginConfig.routes);
201
- for (const [viewName, viewDef] of Object.entries(pluginConfig.views || {})) {
202
- try {
203
- const mod = await import(`/plugins/${viewDef.entry}`);
204
- allViews[viewName] = mod[viewDef.exportName];
205
- } catch { /* plugin view load failure is non-fatal */ }
206
- }
207
- } catch { /* plugins endpoint unreachable — continue without plugins */ }
208
- }
209
-
210
- // -------------------------------------------------------------------------
211
- // Inject user info + sign out into topbar
212
- // -------------------------------------------------------------------------
213
- function injectUserMenu() {
214
- const user = getUser();
215
- if (!user) return;
216
- if ($('#topbar-user-name').length) return; // already injected
217
-
218
- const roleLabelMap = {
219
- admin: 'Admin', manager: 'Manager', editor: 'Editor', subscriber: 'Subscriber'
220
- };
221
- const roleLabel = roleLabelMap[user.role] || user.role;
222
-
223
- $('#topbar-user').html(`
224
- <span id="topbar-user-name" class="topbar-user-name">${escapeHtml(user.name)}</span>
225
- <span class="topbar-role-badge topbar-role-badge--${escapeHtml(user.role)}">${roleLabel}</span>
226
- `);
227
-
228
- $('#topbar-actions').html(`
1
+ import{getSidebarConfig as S}from"./config/sidebar-config.js";import{views as D}from"./views/index.js";import{api as n,getUser as d,isAuthenticated as r,logout as y}from"./api.js";$(()=>{(async()=>{try{const t=await n.settings.get();Domma.theme.init({theme:t.adminTheme||"charcoal-dark",persist:!0})}catch{Domma.theme.init({theme:"charcoal-dark",persist:!0})}})(),R.use(async(t,a,e)=>{if(t.path==="/login")return e();if(!r()){R.navigate("/login");return}e()});let o=null;async function f(){if(!r())return{};try{const[t,a,e,i,s]=await Promise.all([n.pages.list().catch(()=>[]),n.media.list().catch(()=>[]),n.users.list().catch(()=>[]),n.plugins.list().catch(()=>[]),n.collections.list().catch(()=>[])]);return{pages:t.length,media:a.length,users:e.length,plugins:i.filter(c=>c.enabled).length||i.length,collections:s.length}}catch{return{}}}async function p(t){o&&$("#admin-sidebar").empty();const a=await f();o=Domma.elements.sidebar("#admin-sidebar",{header:{title:"CMS Admin",icon:"layout"},items:S(t,a,u),collapsible:!0,collapseAt:992,push:!0,contentSelector:".dashboard-main",top:"60px"}),w(),v()}function w(){$("#admin-sidebar .sidebar-link").each(function(){const t=$(this).find(".sidebar-text").text().trim();t&&$(this).attr("data-tooltip",t)}),E.tooltip("#admin-sidebar [data-tooltip]",{placement:"right"})}function v(){$("#admin-sidebar .sidebar-heading").each(function(){const t=$(this);t.addClass("sidebar-heading--collapsible"),t.append('<span class="sidebar-heading-toggle"><span data-icon="chevron-down"></span></span>'),t.on("click",function(){const a=!t.hasClass("is-collapsed");t.toggleClass("is-collapsed",a);let e=t.next();for(;e.length&&!e.hasClass("sidebar-heading")&&!e.hasClass("sidebar-divider");)e.toggle(!a),e=e.next()})}),Domma.icons.scan("#admin-sidebar")}M.subscribe("router:afterChange",({to:t})=>{o&&t.path!=="/login"&&o.setActive("#"+t.path)});const C=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/navigation",view:"navigation",title:"Navigation - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:a})=>{if(t.path!=="/login"){if($("#admin-sidebar").show(),$("#admin-topbar").show(),a?.path==="/login"){await g();const e=d();e&&p(e.role)}h()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){$(t.target).closest("button, a").length||$(this).closest(".card").toggleClass("card-collapsed")}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash==="#/login")return;const a=document.querySelector("#view-container .view-header button.btn-primary");a&&(t.preventDefault(),a.click())});const m={...D},l=[...C];let u=[];async function g(){if(r())try{const t=await n.plugins.adminConfig();u=t.sidebar||[],t.routes?.length&&l.push(...t.routes);for(const[a,e]of Object.entries(t.views||{}))try{const i=await import(`/plugins/${e.entry}`);m[a]=i[e.exportName]}catch{}}catch{}}function h(){const t=d();if(!t||$("#topbar-user-name").length)return;const e={admin:"Admin",manager:"Manager",editor:"Editor",subscriber:"Subscriber"}[t.role]||t.role;$("#topbar-user").html(`
2
+ <span id="topbar-user-name" class="topbar-user-name">${b(t.name)}</span>
3
+ <span class="topbar-role-badge topbar-role-badge--${b(t.role)}">${e}</span>
4
+ `),$("#topbar-actions").html(`
229
5
  <a href="#/settings" class="topbar-action-link" data-tooltip="Settings" data-tooltip-placement="bottom">
230
6
  <span data-icon="settings"></span>
231
7
  <span>Settings</span>
@@ -234,54 +10,4 @@ $(() => {
234
10
  <span data-icon="log-out"></span>
235
11
  <span>Sign out</span>
236
12
  </a>
237
- `);
238
-
239
- $('#topbar-logout-btn').on('click', (e) => { e.preventDefault(); logout(); });
240
- Domma.icons.scan('#admin-topbar');
241
- E.tooltip('#topbar-actions [data-tooltip]', { placement: 'bottom' });
242
- }
243
-
244
- function escapeHtml(str) {
245
- return String(str)
246
- .replace(/&/g, '&amp;')
247
- .replace(/</g, '&lt;')
248
- .replace(/>/g, '&gt;');
249
- }
250
-
251
- // -------------------------------------------------------------------------
252
- // Boot sequence
253
- // -------------------------------------------------------------------------
254
- (async () => {
255
- await loadPlugins();
256
-
257
- const user = getUser();
258
- if (user) {
259
- await buildSidebar(user.role);
260
- injectUserMenu();
261
- }
262
-
263
- R.init({
264
- container: '#view-container',
265
- routes: allRoutes,
266
- views: allViews,
267
- default: '/',
268
- transitions: { enter: 'fadeIn', leave: 'fadeOut', duration: 150 }
269
- });
270
-
271
- // Patch Domma router to support /* wildcard routes (multi-segment catch-all)
272
- const origExtract = R._extractParams.bind(R);
273
- R._extractParams = function(routePath, actualPath) {
274
- if (routePath.endsWith('/*')) {
275
- const prefix = routePath.slice(0, -2);
276
- return actualPath.startsWith(prefix + '/') ? {} : null;
277
- }
278
- return origExtract(routePath, actualPath);
279
- };
280
- // Only re-trigger if the current URL needs the wildcard patch (e.g. /pages/edit/about)
281
- const currentPath = (window.location.hash || '#/').slice(1) || '/';
282
- const needsWildcard = allRoutes
283
- .filter(r => r.path.endsWith('/*'))
284
- .some(r => currentPath.startsWith(r.path.slice(0, -2) + '/'));
285
- if (needsWildcard) R._handleRouteChange();
286
- })();
287
- });
13
+ `),$("#topbar-logout-btn").on("click",i=>{i.preventDefault(),y()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"})}function b(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}(async()=>{if(!r())window.location.hash="#/login";else{await g();const i=d();i&&(await p(i.role),h())}R.init({container:"#view-container",routes:l,views:m,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}});const t=R._extractParams.bind(R);R._extractParams=function(i,s){if(i.endsWith("/*")){const c=i.slice(0,-2);return s.startsWith(c+"/")?{}:null}return t(i,s)};const a=(window.location.hash||"#/").slice(1)||"/";l.filter(i=>i.path.endsWith("/*")).some(i=>a.startsWith(i.path.slice(0,-2)+"/"))&&R._handleRouteChange()})()});