domma-cms 0.2.1 → 0.5.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 (166) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1200
  3. package/admin/dist/domma/domma-tools.css +2313 -0
  4. package/admin/dist/domma/domma-tools.min.js +10 -0
  5. package/admin/index.html +4 -0
  6. package/admin/js/api.js +1 -242
  7. package/admin/js/app.js +9 -279
  8. package/admin/js/config/sidebar-config.js +1 -115
  9. package/admin/js/lib/card.js +1 -63
  10. package/admin/js/lib/image-editor.js +1 -869
  11. package/admin/js/lib/markdown-toolbar.js +54 -421
  12. package/admin/js/templates/action-editor.html +171 -0
  13. package/admin/js/templates/actions-list.html +19 -0
  14. package/admin/js/templates/api-reference.html +1411 -0
  15. package/admin/js/templates/block-editor.html +158 -0
  16. package/admin/js/templates/blocks.html +8 -0
  17. package/admin/js/templates/collection-editor.html +47 -0
  18. package/admin/js/templates/collection-entries.html +3 -0
  19. package/admin/js/templates/collections.html +51 -4
  20. package/admin/js/templates/documentation.html +258 -0
  21. package/admin/js/templates/form-editor.html +238 -0
  22. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  23. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  24. package/admin/js/templates/layouts.html +44 -7
  25. package/admin/js/templates/login.html +29 -4
  26. package/admin/js/templates/my-profile.html +17 -0
  27. package/admin/js/templates/page-editor.html +48 -0
  28. package/admin/js/templates/pages.html +6 -1
  29. package/admin/js/templates/pro-docs.html +259 -0
  30. package/admin/js/templates/role-editor.html +59 -0
  31. package/admin/js/templates/roles.html +10 -0
  32. package/admin/js/templates/settings.html +137 -18
  33. package/admin/js/templates/tutorials.html +81 -0
  34. package/admin/js/templates/user-editor.html +7 -0
  35. package/admin/js/templates/users.html +3 -1
  36. package/admin/js/templates/view-editor.html +201 -0
  37. package/admin/js/templates/view-preview.html +51 -0
  38. package/admin/js/templates/views-list.html +19 -0
  39. package/admin/js/views/action-editor.js +1 -0
  40. package/admin/js/views/actions-list.js +1 -0
  41. package/admin/js/views/api-reference.js +1 -0
  42. package/admin/js/views/block-editor.js +8 -0
  43. package/admin/js/views/blocks.js +4 -0
  44. package/admin/js/views/collection-editor.js +3 -487
  45. package/admin/js/views/collection-entries.js +1 -484
  46. package/admin/js/views/collections.js +1 -153
  47. package/admin/js/views/dashboard.js +1 -56
  48. package/admin/js/views/documentation.js +1 -12
  49. package/admin/js/views/form-editor.js +8 -0
  50. package/admin/js/views/form-submissions.js +1 -0
  51. package/admin/js/views/forms.js +1 -0
  52. package/admin/js/views/index.js +1 -39
  53. package/admin/js/views/layouts.js +9 -42
  54. package/admin/js/views/login.js +7 -251
  55. package/admin/js/views/media.js +1 -240
  56. package/admin/js/views/my-profile.js +1 -0
  57. package/admin/js/views/navigation.js +14 -212
  58. package/admin/js/views/page-editor.js +72 -661
  59. package/admin/js/views/pages.js +5 -72
  60. package/admin/js/views/plugins.js +13 -90
  61. package/admin/js/views/pro-docs.js +1 -0
  62. package/admin/js/views/role-editor.js +1 -0
  63. package/admin/js/views/roles.js +4 -0
  64. package/admin/js/views/settings.js +3 -199
  65. package/admin/js/views/tutorials.js +1 -12
  66. package/admin/js/views/user-editor.js +1 -88
  67. package/admin/js/views/users.js +4 -76
  68. package/admin/js/views/view-editor.js +1 -0
  69. package/admin/js/views/view-preview.js +1 -0
  70. package/admin/js/views/views-list.js +1 -0
  71. package/bin/cli.js +1 -1
  72. package/config/auth.json +2 -17
  73. package/config/connections.json.bak +9 -0
  74. package/config/connections.json.example +9 -0
  75. package/config/navigation.json +15 -0
  76. package/config/plugins.json +19 -29
  77. package/config/server.json +6 -6
  78. package/config/site.json +17 -6
  79. package/package.json +24 -10
  80. package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
  81. package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
  82. package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
  83. package/plugins/domma-effects/public/celebrations/index.js +1 -535
  84. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
  85. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
  86. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
  87. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
  88. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
  89. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
  90. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
  91. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
  92. package/plugins/example-analytics/stats.json +21 -12
  93. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  94. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  95. package/plugins/theme-roller/config.js +1 -0
  96. package/plugins/theme-roller/plugin.js +233 -0
  97. package/plugins/theme-roller/plugin.json +31 -0
  98. package/plugins/theme-roller/public/active-theme.css +0 -0
  99. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  100. package/public/css/forms.css +1 -0
  101. package/public/css/site.css +1 -302
  102. package/public/js/btt.js +1 -90
  103. package/public/js/cookie-consent.js +1 -61
  104. package/public/js/form-logic-engine.js +1 -0
  105. package/public/js/forms.js +1 -0
  106. package/public/js/site.js +1 -204
  107. package/scripts/build.js +194 -129
  108. package/scripts/pro.js +254 -0
  109. package/scripts/reset.js +33 -8
  110. package/scripts/seed.js +343 -78
  111. package/scripts/setup.js +5 -4
  112. package/server/middleware/auth.js +136 -97
  113. package/server/routes/api/actions.js +200 -0
  114. package/server/routes/api/auth.js +292 -116
  115. package/server/routes/api/blocks.js +84 -0
  116. package/server/routes/api/collections.js +88 -23
  117. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +483 -505
  118. package/server/routes/api/layouts.js +49 -25
  119. package/server/routes/api/media.js +118 -93
  120. package/server/routes/api/navigation.js +40 -37
  121. package/server/routes/api/pages.js +132 -118
  122. package/server/routes/api/plugins.js +6 -3
  123. package/server/routes/api/settings.js +104 -89
  124. package/server/routes/api/users.js +27 -21
  125. package/server/routes/api/views.js +148 -0
  126. package/server/routes/public.js +124 -108
  127. package/server/server.js +269 -173
  128. package/server/services/actions.js +387 -0
  129. package/server/services/adapterRegistry.js +98 -0
  130. package/server/services/adapters/FileAdapter.js +192 -0
  131. package/server/services/adapters/MongoAdapter.js +220 -0
  132. package/server/services/blocks.js +162 -0
  133. package/server/services/collections.js +74 -86
  134. package/server/services/connectionManager.js +102 -0
  135. package/server/services/content.js +312 -307
  136. package/{plugins/form-builder → server/services}/email.js +126 -103
  137. package/server/services/forms.js +173 -0
  138. package/server/services/markdown.js +1378 -648
  139. package/server/services/permissionRegistry.js +173 -0
  140. package/server/services/presetCollections.js +251 -0
  141. package/server/services/renderer.js +75 -1
  142. package/server/services/roles.js +227 -0
  143. package/server/services/rowAccess.js +104 -0
  144. package/server/services/userProfiles.js +199 -0
  145. package/server/services/users.js +281 -212
  146. package/server/services/views.js +280 -0
  147. package/server/templates/page.html +119 -113
  148. package/plugins/form-builder/admin/templates/form-editor.html +0 -171
  149. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  150. package/plugins/form-builder/admin/views/form-editor.js +0 -1442
  151. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  152. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  153. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  154. package/plugins/form-builder/config.js +0 -9
  155. package/plugins/form-builder/data/forms/consent.json +0 -104
  156. package/plugins/form-builder/data/forms/contact-details.json +0 -63
  157. package/plugins/form-builder/data/forms/contacts.json +0 -66
  158. package/plugins/form-builder/data/submissions/consent.json +0 -13
  159. package/plugins/form-builder/data/submissions/contact-details.json +0 -1
  160. package/plugins/form-builder/data/submissions/contacts.json +0 -26
  161. package/plugins/form-builder/plugin.json +0 -52
  162. package/plugins/form-builder/public/form-logic-engine.js +0 -568
  163. package/plugins/form-builder/public/inject-body.html +0 -352
  164. package/plugins/form-builder/public/inject-head.html +0 -58
  165. package/plugins/form-builder/public/package.json +0 -1
  166. package/scripts/copy-domma.js +0 -48
@@ -1,255 +1,11 @@
1
- /**
2
- * Login / First-time Setup View
3
- *
4
- * Flow:
5
- * - Already logged in → redirect to dashboard
6
- * - needsSetup = true → show setup form → onboarding wizard (site → theme → done)
7
- * - needsSetup = false → show login form → redirect to dashboard
8
- */
9
- import {api, isAuthenticated, setAuthData} from '../api.js';
10
-
11
- // ---------------------------------------------------------------------------
12
- // Blueprints
13
- // ---------------------------------------------------------------------------
14
-
15
- const setupBlueprint = {
16
- name: {
17
- type: 'string',
18
- required: true,
19
- minLength: 2,
20
- label: 'Full Name',
21
- formConfig: { placeholder: 'Your name', autocomplete: 'name' }
22
- },
23
- email: {
24
- type: 'email',
25
- required: true,
26
- label: 'Email Address',
27
- formConfig: { placeholder: 'admin@example.com', autocomplete: 'email' }
28
- },
29
- password: {
30
- type: 'password',
31
- required: true,
32
- minLength: 8,
33
- label: 'Password',
34
- formConfig: { placeholder: '••••••••', autocomplete: 'new-password', tooltip: 'Minimum 8 characters' }
35
- }
36
- };
37
-
38
- const loginBlueprint = {
39
- email: {
40
- type: 'email',
41
- required: true,
42
- label: 'Email Address',
43
- formConfig: { placeholder: 'you@example.com', autocomplete: 'email' }
44
- },
45
- password: {
46
- type: 'password',
47
- required: true,
48
- label: 'Password',
49
- formConfig: { placeholder: '••••••••', autocomplete: 'current-password' }
50
- }
51
- };
52
-
53
- const THEMES = [
54
- { id: 'charcoal-dark', label: 'Charcoal', dark: true, primary: '#5b8cff', bg: '#1a1d23' },
55
- { id: 'charcoal-light', label: 'Charcoal', dark: false, primary: '#4a7aff', bg: '#f4f5f7' },
56
- { id: 'ocean-dark', label: 'Ocean', dark: true, primary: '#00b4d8', bg: '#0d1b2a' },
57
- { id: 'ocean-light', label: 'Ocean', dark: false, primary: '#0096c7', bg: '#e8f4f8' },
58
- { id: 'forest-dark', label: 'Forest', dark: true, primary: '#52b788', bg: '#1a231e' },
59
- { id: 'forest-light', label: 'Forest', dark: false, primary: '#40916c', bg: '#f0f7f4' },
60
- { id: 'sunset-dark', label: 'Sunset', dark: true, primary: '#ff6b6b', bg: '#1f1a1a' },
61
- { id: 'sunset-light', label: 'Sunset', dark: false, primary: '#e05555', bg: '#fff0f0' },
62
- { id: 'royal-dark', label: 'Royal', dark: true, primary: '#9b59b6', bg: '#1a1525' },
63
- { id: 'royal-light', label: 'Royal', dark: false, primary: '#8e44ad', bg: '#f5f0fa' },
64
- { id: 'lemon-dark', label: 'Lemon', dark: true, primary: '#f1c40f', bg: '#1a1a10' },
65
- { id: 'lemon-light', label: 'Lemon', dark: false, primary: '#d4ac0d', bg: '#fffef0' },
66
- { id: 'silver-dark', label: 'Silver', dark: true, primary: '#95a5a6', bg: '#1c1e20' },
67
- { id: 'silver-light', label: 'Silver', dark: false, primary: '#7f8c8d', bg: '#f2f3f4' },
68
- {id: 'grayve-dark', label: 'Grayve', dark: true, primary: '#00bcd4', bg: '#1a1e21'},
69
- {id: 'grayve-light', label: 'Grayve', dark: false, primary: '#00838f', bg: '#ffffff'},
70
- ];
71
-
72
- export const loginView = {
73
- templateUrl: '/admin/js/templates/login.html',
74
-
75
- async onMount($container) {
76
- if (isAuthenticated()) {
77
- R.navigate('/');
78
- return;
79
- }
80
-
81
- let needsSetup = false;
82
- try {
83
- const status = await api.auth.setupStatus();
84
- needsSetup = status.needsSetup;
85
- } catch {
86
- E.toast('Could not reach the server.', { type: 'error' });
87
- }
88
-
89
- if (needsSetup) {
90
- showStep($container, 'setup');
91
- bindSetup($container);
92
- } else {
93
- showStep($container, 'login');
94
- bindLogin($container);
95
- }
96
-
97
- Domma.icons.scan();
98
- }
99
- };
100
-
101
- // ---------------------------------------------------------------------------
102
- // Panel switching
103
- // ---------------------------------------------------------------------------
104
-
105
- const PANELS = ['setup', 'onboarding-site', 'onboarding-theme', 'onboarding-done', 'login'];
106
-
107
- function showStep($container, name) {
108
- PANELS.forEach(p => $container.find(`#${p}-panel`).hide());
109
- $container.find(`#${name}-panel`).show();
110
- Domma.icons.scan();
111
- }
112
-
113
- // ---------------------------------------------------------------------------
114
- // Setup step
115
- // ---------------------------------------------------------------------------
116
-
117
- function bindSetup($container) {
118
- F.render('#setup-form-container', setupBlueprint, {}, {
119
- layout: 'stacked',
120
- submitText: 'Create admin account',
121
- onSubmit: async (data) => {
122
- try {
123
- const result = await api.auth.setup(data);
124
- setAuthData(result);
125
- showStep($container, 'onboarding-site');
126
- bindOnboardingSite($container);
127
- } catch (err) {
128
- E.toast(err.message || 'Setup failed. Please try again.', { type: 'error' });
129
- return false;
130
- }
131
- }
132
- });
133
- }
134
-
135
- // ---------------------------------------------------------------------------
136
- // Onboarding Step 2 — Site Identity
137
- // ---------------------------------------------------------------------------
138
-
139
- function bindOnboardingSite($container) {
140
- $container.find('#ob-site-skip').on('click', (e) => {
141
- e.preventDefault();
142
- showStep($container, 'onboarding-theme');
143
- buildThemeGrid($container);
144
- bindOnboardingTheme($container);
145
- });
146
-
147
- $container.find('#ob-site-btn').on('click', async () => {
148
- $container.find('#ob-site-error').hide();
149
- const title = $container.find('#ob-title').val().trim();
150
- const tagline = $container.find('#ob-tagline').val().trim();
151
-
152
- const $btn = $container.find('#ob-site-btn').prop('disabled', true).text('Saving…');
153
- try {
154
- // Fetch current settings then merge title + tagline
155
- const current = await api.settings.get();
156
- await api.settings.save({
157
- ...current,
158
- title: title || current.title,
159
- tagline: tagline || current.tagline,
160
- seo: {
161
- ...(current.seo || {}),
162
- defaultTitle: title || (current.seo && current.seo.defaultTitle)
163
- }
164
- });
165
-
166
- // Also update nav brand text
167
- const nav = await api.navigation.get();
168
- await api.navigation.save({
169
- ...nav,
170
- brand: { ...(nav.brand || {}), text: title || nav.brand.text }
171
- });
172
-
173
- showStep($container, 'onboarding-theme');
174
- buildThemeGrid($container);
175
- bindOnboardingTheme($container);
176
- } catch (err) {
177
- showError($container, 'ob-site-error', err.message || 'Could not save site details.');
178
- } finally {
179
- $btn.prop('disabled', false).text('Continue');
180
- }
181
- });
182
- }
183
-
184
- // ---------------------------------------------------------------------------
185
- // Onboarding Step 3 — Theme Picker
186
- // ---------------------------------------------------------------------------
187
-
188
- function buildThemeGrid($container) {
189
- const $grid = $container.find('#theme-grid').empty();
190
- THEMES.forEach(t => {
191
- const $card = $(`
192
- <div class="theme-swatch" data-theme="${t.id}" title="${t.id}">
193
- <div class="theme-swatch-preview" style="background:${t.bg}">
194
- <div class="theme-swatch-accent" style="background:${t.primary}"></div>
1
+ import{api as s,isAuthenticated as m,setAuthData as d}from"../api.js";const u={name:{type:"string",required:!0,minLength:2,label:"Full Name",formConfig:{placeholder:"Your name",autocomplete:"name"}},email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"admin@example.com",autocomplete:"email"}},password:{type:"password",required:!0,minLength:8,label:"Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password",tooltip:"Minimum 8 characters"}}},p={email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"you@example.com",autocomplete:"email"}},password:{type:"password",required:!0,label:"Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"current-password"}}},b=[{id:"charcoal-dark",label:"Charcoal",dark:!0,primary:"#5b8cff",bg:"#1a1d23"},{id:"charcoal-light",label:"Charcoal",dark:!1,primary:"#4a7aff",bg:"#f4f5f7"},{id:"ocean-dark",label:"Ocean",dark:!0,primary:"#00b4d8",bg:"#0d1b2a"},{id:"ocean-light",label:"Ocean",dark:!1,primary:"#0096c7",bg:"#e8f4f8"},{id:"forest-dark",label:"Forest",dark:!0,primary:"#52b788",bg:"#1a231e"},{id:"forest-light",label:"Forest",dark:!1,primary:"#40916c",bg:"#f0f7f4"},{id:"sunset-dark",label:"Sunset",dark:!0,primary:"#ff6b6b",bg:"#1f1a1a"},{id:"sunset-light",label:"Sunset",dark:!1,primary:"#e05555",bg:"#fff0f0"},{id:"royal-dark",label:"Royal",dark:!0,primary:"#9b59b6",bg:"#1a1525"},{id:"royal-light",label:"Royal",dark:!1,primary:"#8e44ad",bg:"#f5f0fa"},{id:"lemon-dark",label:"Lemon",dark:!0,primary:"#f1c40f",bg:"#1a1a10"},{id:"lemon-light",label:"Lemon",dark:!1,primary:"#d4ac0d",bg:"#fffef0"},{id:"silver-dark",label:"Silver",dark:!0,primary:"#95a5a6",bg:"#1c1e20"},{id:"silver-light",label:"Silver",dark:!1,primary:"#7f8c8d",bg:"#f2f3f4"},{id:"grayve-dark",label:"Grayve",dark:!0,primary:"#00bcd4",bg:"#1a1e21"},{id:"grayve-light",label:"Grayve",dark:!1,primary:"#00838f",bg:"#ffffff"},{id:"mint-dark",label:"Mint",dark:!0,primary:"#00c896",bg:"#0f1f1a"},{id:"mint-light",label:"Mint",dark:!1,primary:"#00a878",bg:"#f0faf6"}];export const loginView={templateUrl:"/admin/js/templates/login.html",async onMount(e){if(m()){R.navigate("/");return}const t=window.location.hash,a=t.match(/[?&]token=([a-f0-9]{64})/);if(t.startsWith("#/reset-password")&&a){i(e,"reset"),C(e,a[1]),Domma.icons.scan();return}if(t.startsWith("#/reset-password")){R.navigate("/login");return}let r=!1;try{r=(await s.auth.setupStatus()).needsSetup}catch{E.toast("Could not reach the server.",{type:"error"})}r?(i(e,"setup"),w(e)):(i(e,"login"),v(e)),Domma.icons.scan()}};const g={email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"you@example.com",autocomplete:"email"}}},h={password:{type:"password",required:!0,minLength:8,label:"New Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password",tooltip:"Minimum 8 characters"}},confirmPassword:{type:"password",required:!0,label:"Confirm Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password"}}},y=["setup","onboarding-site","onboarding-theme","onboarding-done","login","forgot","forgot-success","reset"];function i(e,t){y.forEach(a=>e.find(`#${a}-panel`).hide()),e.find(`#${t}-panel`).show(),Domma.icons.scan()}function n(e,t,a){e.find(`#${t}`).text(a).show()}function w(e){F.render("#setup-form-container",u,{},{layout:"stacked",submitText:"Create admin account",onSubmit:async t=>{try{const a=await s.auth.setup(t);d(a),i(e,"onboarding-site"),k(e)}catch(a){return E.toast(a.message||"Setup failed. Please try again.",{type:"error"}),!1}}})}function k(e){e.find("#ob-site-skip").on("click",t=>{t.preventDefault(),i(e,"onboarding-theme"),c(e),f(e)}),e.find("#ob-site-btn").on("click",async()=>{e.find("#ob-site-error").hide();const t=e.find("#ob-title").val().trim(),a=e.find("#ob-tagline").val().trim(),r=e.find("#ob-site-btn").prop("disabled",!0).text("Saving\u2026");try{const o=await s.settings.get();await s.settings.save({...o,title:t||o.title,tagline:a||o.tagline,seo:{...o.seo||{},defaultTitle:t||o.seo&&o.seo.defaultTitle}});const l=await s.navigation.get();await s.navigation.save({...l,brand:{...l.brand||{},text:t||l.brand.text}}),i(e,"onboarding-theme"),c(e),f(e)}catch(o){n(e,"ob-site-error",o.message||"Could not save site details.")}finally{r.prop("disabled",!1).text("Continue")}})}function c(e){const t=e.find("#theme-grid").empty();b.forEach(a=>{const r=$(`
2
+ <div class="theme-swatch" data-theme="${a.id}" title="${a.id}">
3
+ <div class="theme-swatch-preview" style="background:${a.bg}">
4
+ <div class="theme-swatch-accent" style="background:${a.primary}"></div>
195
5
  </div>
196
6
  <div class="theme-swatch-label">
197
- <span>${t.label}</span>
198
- <span class="theme-swatch-mode">${t.dark ? 'Dark' : 'Light'}</span>
7
+ <span>${a.label}</span>
8
+ <span class="theme-swatch-mode">${a.dark?"Dark":"Light"}</span>
199
9
  </div>
200
10
  </div>
201
- `);
202
- $card.on('click', () => {
203
- $container.find('.theme-swatch').removeClass('selected');
204
- $card.addClass('selected');
205
- });
206
- $grid.append($card);
207
- });
208
-
209
- // Default selection
210
- $container.find('[data-theme="charcoal-dark"]').addClass('selected');
211
- }
212
-
213
- function bindOnboardingTheme($container) {
214
- $container.find('#ob-theme-skip').on('click', (e) => {
215
- e.preventDefault();
216
- showStep($container, 'onboarding-done');
217
- });
218
-
219
- $container.find('#ob-theme-btn').on('click', async () => {
220
- $container.find('#ob-theme-error').hide();
221
- const selected = $container.find('.theme-swatch.selected').attr('data-theme') || 'charcoal-dark';
222
-
223
- const $btn = $container.find('#ob-theme-btn').prop('disabled', true).text('Applying…');
224
- try {
225
- const current = await api.settings.get();
226
- await api.settings.save({ ...current, theme: selected });
227
- showStep($container, 'onboarding-done');
228
- } catch (err) {
229
- showError($container, 'ob-theme-error', err.message || 'Could not save theme.');
230
- } finally {
231
- $btn.prop('disabled', false).text('Apply theme');
232
- }
233
- });
234
- }
235
-
236
- // ---------------------------------------------------------------------------
237
- // Login step
238
- // ---------------------------------------------------------------------------
239
-
240
- function bindLogin($container) {
241
- F.render('#login-form-container', loginBlueprint, {}, {
242
- layout: 'stacked',
243
- submitText: 'Sign in',
244
- onSubmit: async (data) => {
245
- try {
246
- const result = await api.auth.login(data);
247
- setAuthData(result);
248
- R.navigate('/');
249
- } catch (err) {
250
- E.toast(err.message || 'Invalid credentials. Please try again.', { type: 'error' });
251
- return false;
252
- }
253
- }
254
- });
255
- }
11
+ `);r.on("click",()=>{e.find(".theme-swatch").removeClass("selected"),r.addClass("selected")}),t.append(r)}),e.find('[data-theme="charcoal-dark"]').addClass("selected")}function f(e){e.find("#ob-theme-skip").on("click",t=>{t.preventDefault(),i(e,"onboarding-done")}),e.find("#ob-theme-btn").on("click",async()=>{e.find("#ob-theme-error").hide();const t=e.find(".theme-swatch.selected").attr("data-theme")||"charcoal-dark",a=e.find("#ob-theme-btn").prop("disabled",!0).text("Applying\u2026");try{const r=await s.settings.get();await s.settings.save({...r,theme:t}),i(e,"onboarding-done")}catch(r){n(e,"ob-theme-error",r.message||"Could not save theme.")}finally{a.prop("disabled",!1).text("Apply theme")}})}function v(e){F.render("#login-form-container",p,{},{layout:"stacked",submitText:"Sign in",onSubmit:async t=>{try{const a=await s.auth.login(t);d(a),R.navigate("/")}catch(a){return E.toast(a.message||"Invalid credentials. Please try again.",{type:"error"}),!1}}}),e.find("#forgot-link").on("click",t=>{t.preventDefault(),i(e,"forgot"),S(e)})}function S(e){F.render("#forgot-form-container",g,{},{layout:"stacked",submitText:"Send reset link",onSubmit:async a=>{try{await s.auth.forgotPassword(a.email)}catch{}i(e,"forgot-success"),Domma.icons.scan()}});const t=a=>{a.preventDefault(),i(e,"login")};e.find("#forgot-back-link").on("click",t),e.find("#forgot-success-back-link").on("click",t)}function C(e,t){F.render("#reset-form-container",h,{},{layout:"stacked",submitText:"Set new password",onSubmit:async a=>{if(a.password!==a.confirmPassword)return E.toast("Passwords do not match.",{type:"error"}),!1;try{await s.auth.resetPassword(t,a.password),E.toast("Password updated. Please sign in.",{type:"success"}),window.location.hash="#/login"}catch(r){return E.toast(r.message||"Link expired or invalid.",{type:"error"}),!1}}})}
@@ -1,240 +1 @@
1
- /**
2
- * Media View
3
- */
4
- import {api} from '../api.js';
5
- import {openImageEditor} from '../lib/image-editor.js';
6
-
7
- export const mediaView = {
8
- templateUrl: '/admin/js/templates/media.html',
9
-
10
- async onMount($container) {
11
- const renderGrid = (files) => {
12
- const grid = $container.find('#media-grid').empty().get(0);
13
- if (!files.length) {
14
- const empty = document.createElement('p');
15
- empty.className = 'text-muted';
16
- empty.textContent = 'No media files yet. Upload one above.';
17
- grid.appendChild(empty);
18
- return;
19
- }
20
- files.forEach(file => {
21
- const isImage = /\.(png|jpe?g|gif|webp|svg)$/i.test(file.name);
22
- const isEditable = /\.(png|jpe?g|gif|webp|tiff?)$/i.test(file.name);
23
-
24
- const card = document.createElement('div');
25
- card.className = 'media-card';
26
-
27
- // Preview
28
- const preview = document.createElement('div');
29
- preview.className = 'media-preview';
30
- if (isImage) {
31
- const img = document.createElement('img');
32
- img.src = file.url + '?t=' + Date.now();
33
- img.alt = file.name;
34
- img.className = 'media-thumb';
35
- preview.appendChild(img);
36
- } else {
37
- const thumb = document.createElement('div');
38
- thumb.className = 'media-thumb media-thumb--file';
39
- const icon = document.createElement('span');
40
- icon.setAttribute('data-icon', 'file');
41
- thumb.appendChild(icon);
42
- preview.appendChild(thumb);
43
- }
44
- card.appendChild(preview);
45
-
46
- // Info
47
- const info = document.createElement('div');
48
- info.className = 'media-info';
49
-
50
- const nameSpan = document.createElement('span');
51
- nameSpan.className = 'media-name';
52
- nameSpan.title = 'Double-click to rename';
53
- nameSpan.textContent = file.name;
54
-
55
- const sizeSpan = document.createElement('span');
56
- sizeSpan.className = 'media-size';
57
- sizeSpan.textContent = formatBytes(file.size);
58
- info.appendChild(nameSpan);
59
- info.appendChild(sizeSpan);
60
- card.appendChild(info);
61
-
62
- // Inline rename: double-click the filename to edit
63
- nameSpan.addEventListener('dblclick', () => startRename(nameSpan, file, renderGrid));
64
-
65
- // Actions
66
- const actions = document.createElement('div');
67
- actions.className = 'media-actions';
68
-
69
- const copyBtn = document.createElement('button');
70
- copyBtn.className = 'btn btn-sm btn-ghost';
71
- const copyIcon = document.createElement('span');
72
- copyIcon.setAttribute('data-icon', 'copy');
73
- copyBtn.appendChild(copyIcon);
74
- copyBtn.addEventListener('click', () => {
75
- navigator.clipboard.writeText(window.location.origin + file.url)
76
- .then(() => E.toast('URL copied.', {type: 'success'}))
77
- .catch(() => E.toast('Copy failed.', {type: 'error'}));
78
- });
79
-
80
- const renameBtn = document.createElement('button');
81
- renameBtn.className = 'btn btn-sm btn-ghost';
82
- const renameIcon = document.createElement('span');
83
- renameIcon.setAttribute('data-icon', 'type');
84
- renameBtn.appendChild(renameIcon);
85
- renameBtn.addEventListener('click', () => startRename(nameSpan, file, renderGrid));
86
-
87
- let editBtn = null;
88
- if (isEditable) {
89
- editBtn = document.createElement('button');
90
- editBtn.className = 'btn btn-sm btn-ghost';
91
- const editIcon = document.createElement('span');
92
- editIcon.setAttribute('data-icon', 'edit');
93
- editBtn.appendChild(editIcon);
94
- editBtn.addEventListener('click', async () => {
95
- const saved = await openImageEditor(file);
96
- if (saved) renderGrid(await api.media.list().catch(() => []));
97
- });
98
- }
99
-
100
- const deleteBtn = document.createElement('button');
101
- deleteBtn.className = 'btn btn-sm btn-danger';
102
- const deleteIcon = document.createElement('span');
103
- deleteIcon.setAttribute('data-icon', 'trash');
104
- deleteBtn.appendChild(deleteIcon);
105
- deleteBtn.addEventListener('click', async () => {
106
- if (!await E.confirm(`Delete "${file.name}"?`)) return;
107
- try {
108
- await api.media.delete(file.name);
109
- E.toast('File deleted.', {type: 'success'});
110
- renderGrid(await api.media.list().catch(() => []));
111
- } catch {
112
- E.toast('Delete failed.', {type: 'error'});
113
- }
114
- });
115
-
116
- actions.appendChild(copyBtn);
117
- actions.appendChild(renameBtn);
118
- if (editBtn) actions.appendChild(editBtn);
119
- actions.appendChild(deleteBtn);
120
- card.appendChild(actions);
121
- grid.appendChild(card);
122
-
123
- // Tooltips must be registered after the element is in the DOM
124
- E.tooltip(copyBtn, {content: 'Copy URL', position: 'top'});
125
- E.tooltip(renameBtn, {content: 'Rename', position: 'top'});
126
- if (editBtn) E.tooltip(editBtn, {content: 'Edit image', position: 'top'});
127
- E.tooltip(deleteBtn, {content: 'Delete file', position: 'top'});
128
- });
129
-
130
- Domma.icons.scan();
131
- Domma.effects.reveal('.media-card', { animation: 'fade', stagger: 40, duration: 350 });
132
- };
133
-
134
- const initialFiles = await api.media.list().catch(() => []);
135
- renderGrid(initialFiles);
136
-
137
- Domma.effects.ripple('#upload-btn');
138
- $container.find('#upload-btn').on('click', () => $container.find('#file-input').get(0).click());
139
-
140
- $container.find('#file-input').on('change', async function () {
141
- if (!this.files.length) return;
142
- const formData = new FormData();
143
- for (const file of this.files) formData.append('file', file);
144
-
145
- const btn = $container.find('#upload-btn').get(0);
146
- const textNode = btn.lastChild;
147
- btn.disabled = true;
148
- textNode.textContent = ' Uploading…';
149
-
150
- try {
151
- const resp = await fetch('/api/media', {
152
- method: 'POST',
153
- body: formData,
154
- headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}
155
- });
156
- if (!resp.ok) throw new Error('Upload failed');
157
- E.toast('File uploaded.', { type: 'success' });
158
- renderGrid(await api.media.list().catch(() => []));
159
- } catch {
160
- E.toast('Upload failed.', { type: 'error' });
161
- } finally {
162
- btn.disabled = false;
163
- textNode.textContent = ' Upload';
164
- this.value = '';
165
- }
166
- });
167
- }
168
- };
169
-
170
- /**
171
- * Replace a filename span with an inline input for renaming.
172
- * Confirms with Enter, cancels with Escape or blur.
173
- *
174
- * @param {HTMLElement} nameSpan
175
- * @param {{ name: string, url: string }} file
176
- * @param {Function} renderGrid
177
- */
178
- function startRename(nameSpan, file, renderGrid) {
179
- if (nameSpan.querySelector('input')) return; // already editing
180
-
181
- const original = file.name;
182
- const dotIndex = original.lastIndexOf('.');
183
- const baseName = dotIndex > 0 ? original.slice(0, dotIndex) : original;
184
-
185
- const input = document.createElement('input');
186
- input.type = 'text';
187
- input.className = 'media-rename-input';
188
- input.value = original;
189
- input.title = '';
190
-
191
- nameSpan.textContent = '';
192
- nameSpan.appendChild(input);
193
-
194
- input.focus();
195
- input.setSelectionRange(0, baseName.length);
196
-
197
- async function confirm() {
198
- const newName = input.value.trim();
199
- if (!newName || newName === original) return cancel();
200
- input.disabled = true;
201
- try {
202
- await api.media.rename(original, newName);
203
- E.toast('File renamed.', {type: 'success'});
204
- renderGrid(await api.media.list().catch(() => []));
205
- } catch (err) {
206
- E.toast(err.message || 'Rename failed.', {type: 'error'});
207
- cancel();
208
- }
209
- }
210
-
211
- function cancel() {
212
- nameSpan.textContent = original;
213
- nameSpan.title = 'Double-click to rename';
214
- }
215
-
216
- input.addEventListener('keydown', (e) => {
217
- if (e.key === 'Enter') {
218
- e.preventDefault();
219
- confirm();
220
- }
221
- if (e.key === 'Escape') {
222
- e.preventDefault();
223
- cancel();
224
- }
225
- });
226
-
227
- input.addEventListener('blur', () => {
228
- // Short delay so Enter keydown fires before blur
229
- setTimeout(() => {
230
- if (nameSpan.contains(input)) cancel();
231
- }, 150);
232
- });
233
- }
234
-
235
- function formatBytes(bytes) {
236
- if (!bytes) return '0 B';
237
- const k = 1024, sizes = ['B', 'KB', 'MB', 'GB'];
238
- const i = Math.floor(Math.log(bytes) / Math.log(k));
239
- return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
240
- }
1
+ import{api as m}from"../api.js";import{openImageEditor as B}from"../lib/image-editor.js";export const mediaView={templateUrl:"/admin/js/templates/media.html",async onMount(t){const i=c=>{const d=t.find("#media-grid").empty().get(0);if(!c.length){const e=document.createElement("p");e.className="text-muted",e.textContent="No media files yet. Upload one above.",d.appendChild(e);return}c.forEach(e=>{const r=/\.(png|jpe?g|gif|webp|svg)$/i.test(e.name),p=/\.(png|jpe?g|gif|webp|tiff?)$/i.test(e.name),n=document.createElement("div");n.className="media-card";const u=document.createElement("div");if(u.className="media-preview",r){const o=document.createElement("img");o.src=e.url+"?t="+Date.now(),o.alt=e.name,o.className="media-thumb",u.appendChild(o)}else{const o=document.createElement("div");o.className="media-thumb media-thumb--file";const w=document.createElement("span");w.setAttribute("data-icon","file"),o.appendChild(w),u.appendChild(o)}n.appendChild(u);const C=document.createElement("div");C.className="media-info";const l=document.createElement("span");l.className="media-name",l.title="Double-click to rename",l.textContent=e.name;const v=document.createElement("span");v.className="media-size",v.textContent=L(e.size),C.appendChild(l),C.appendChild(v),n.appendChild(C),l.addEventListener("dblclick",()=>D(l,e,i));const h=document.createElement("div");h.className="media-actions";const b=document.createElement("button");b.className="btn btn-sm btn-ghost";const N=document.createElement("span");N.setAttribute("data-icon","copy"),b.appendChild(N),b.addEventListener("click",()=>{navigator.clipboard.writeText(window.location.origin+e.url).then(()=>E.toast("URL copied.",{type:"success"})).catch(()=>E.toast("Copy failed.",{type:"error"}))});const g=document.createElement("button");g.className="btn btn-sm btn-ghost";const k=document.createElement("span");k.setAttribute("data-icon","type"),g.appendChild(k),g.addEventListener("click",()=>D(l,e,i));let s=null;if(p){s=document.createElement("button"),s.className="btn btn-sm btn-ghost";const o=document.createElement("span");o.setAttribute("data-icon","edit"),s.appendChild(o),s.addEventListener("click",async()=>{await B(e)&&i(await m.media.list().catch(()=>[]))})}const y=document.createElement("button");y.className="btn btn-sm btn-danger";const x=document.createElement("span");x.setAttribute("data-icon","trash"),y.appendChild(x),y.addEventListener("click",async()=>{if(await E.confirm(`Delete "${e.name}"?`))try{await m.media.delete(e.name),E.toast("File deleted.",{type:"success"}),i(await m.media.list().catch(()=>[]))}catch{E.toast("Delete failed.",{type:"error"})}}),h.appendChild(b),h.appendChild(g),s&&h.appendChild(s),h.appendChild(y),n.appendChild(h),d.appendChild(n),E.tooltip(b,{content:"Copy URL",position:"top"}),E.tooltip(g,{content:"Rename",position:"top"}),s&&E.tooltip(s,{content:"Edit image",position:"top"}),E.tooltip(y,{content:"Delete file",position:"top"})}),Domma.icons.scan(),Domma.effects.reveal(".media-card",{animation:"fade",stagger:40,duration:350})},f=E.loader(t.get(0),{type:"dots"}),a=await m.media.list().catch(()=>[]);f.destroy(),i(a),Domma.effects.ripple("#upload-btn"),t.find("#upload-btn").on("click",()=>t.find("#file-input").get(0).click()),t.find("#file-input").on("change",async function(){if(!this.files.length)return;const c=new FormData;for(const r of this.files)c.append("file",r);const d=t.find("#upload-btn").get(0),e=d.lastChild;d.disabled=!0,e.textContent=" Uploading\u2026";try{if(!(await fetch("/api/media",{method:"POST",body:c,headers:{Authorization:"Bearer "+(S.get("auth_token")||"")}})).ok)throw new Error("Upload failed");E.toast("File uploaded.",{type:"success"}),i(await m.media.list().catch(()=>[]))}catch{E.toast("Upload failed.",{type:"error"})}finally{d.disabled=!1,e.textContent=" Upload",this.value=""}})}};function D(t,i,f){if(t.querySelector("input"))return;const a=i.name,c=a.lastIndexOf("."),d=c>0?a.slice(0,c):a,e=document.createElement("input");e.type="text",e.className="media-rename-input",e.value=a,e.title="",t.textContent="",t.appendChild(e),e.focus(),e.setSelectionRange(0,d.length);async function r(){const n=e.value.trim();if(!n||n===a)return p();e.disabled=!0;try{await m.media.rename(a,n),E.toast("File renamed.",{type:"success"}),f(await m.media.list().catch(()=>[]))}catch(u){E.toast(u.message||"Rename failed.",{type:"error"}),p()}}function p(){t.textContent=a,t.title="Double-click to rename"}e.addEventListener("keydown",n=>{n.key==="Enter"&&(n.preventDefault(),r()),n.key==="Escape"&&(n.preventDefault(),p())}),e.addEventListener("blur",()=>{setTimeout(()=>{t.contains(e)&&p()},150)})}function L(t){if(!t)return"0 B";const i=1024,f=["B","KB","MB","GB"],a=Math.floor(Math.log(t)/Math.log(i));return`${(t/Math.pow(i,a)).toFixed(1)} ${f[a]}`}
@@ -0,0 +1 @@
1
+ import{api as l}from"../api.js";function d(s){return{string:"string",email:"email",tel:"string",number:"number",textarea:"textarea",select:"select",radio:"select",checkbox:"boolean","checkbox-group":"select",date:"string",time:"string",url:"string",hidden:"string"}[s]||"string"}export const myProfileView={templateUrl:"/admin/js/templates/my-profile.html",async onMount(s){const[r,p]=await Promise.all([l.auth.me().catch(()=>null),l.collections.get("user-profiles").catch(()=>null)]);if(!r){E.toast("Could not load your account details.",{type:"error"});return}const m={name:{type:"string",required:!0,minLength:2,label:"Full Name",formConfig:{placeholder:"Jane Smith"}},email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"jane@example.com"}},password:{type:"password",required:!1,minLength:0,label:"New Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password",tooltip:"Leave blank to keep your current password"}}},u={name:r.name||"",email:r.email||""};F.render("#account-form-container",m,u,{layout:"stacked",submitText:"Save Account",onSubmit:async t=>{if(t.password&&t.password.length<8)return E.toast("Password must be at least 8 characters.",{type:"warning"}),!1;const a={name:t.name,email:t.email};t.password&&(a.password=t.password);try{await l.auth.updateMe(a),E.toast("Account updated.",{type:"success"})}catch(e){return E.toast(`Save failed: ${e.message||"Unknown error"}`,{type:"error"}),!1}}});const i=p?.fields||[];if(i.length>0){s.find("#profile-card").show();const t={},a={};for(const e of i){const o={type:d(e.type),label:e.label||e.name};e.required&&(o.required=!0),e.placeholder&&(o.formConfig={placeholder:e.placeholder}),e.options&&Array.isArray(e.options)&&(o.options=e.options.map(n=>typeof n=="string"?{value:n,label:n}:n)),t[e.name]=o,a[e.name]=r.profile?.[e.name]??""}F.render("#profile-form-container",t,a,{layout:"stacked",submitText:"Save Profile",onSubmit:async e=>{try{await l.auth.updateMe({profile:e}),E.toast("Profile saved.",{type:"success"})}catch(c){return E.toast(`Save failed: ${c.message||"Unknown error"}`,{type:"error"}),!1}}})}Domma.icons.scan()}};