webspresso 0.0.76 → 0.0.78

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 (35) hide show
  1. package/README.md +6 -1
  2. package/bin/commands/db-scaffold.js +81 -0
  3. package/bin/utils/model-migrations.js +211 -0
  4. package/bin/webspresso.js +2 -0
  5. package/core/content/cache.js +64 -0
  6. package/core/content/field-types.js +180 -0
  7. package/core/content/index.js +30 -0
  8. package/core/content/renderer.js +84 -0
  9. package/core/content/schema.js +75 -0
  10. package/core/content/service.js +400 -0
  11. package/core/content/types.js +59 -0
  12. package/index.d.ts +17 -0
  13. package/index.js +7 -0
  14. package/package.json +1 -1
  15. package/plugins/admin-panel/app.js +7 -7
  16. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +41 -0
  17. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +99 -15
  18. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +2 -2
  19. package/plugins/admin-panel/field-renderers/file-upload.js +108 -27
  20. package/plugins/admin-panel/index.js +17 -18
  21. package/plugins/admin-panel/modules/menu.js +1 -0
  22. package/plugins/content/admin/content-entries-component.js +291 -0
  23. package/plugins/content/admin/content-types-component.js +250 -0
  24. package/plugins/content/api-handlers.js +157 -0
  25. package/plugins/content/client/inline-edit.css +296 -0
  26. package/plugins/content/client/inline-edit.js +366 -0
  27. package/plugins/content/helpers.js +77 -0
  28. package/plugins/content/index.js +231 -0
  29. package/plugins/content/migration-template.js +54 -0
  30. package/plugins/content/models/content-entry.js +45 -0
  31. package/plugins/content/models/content-type.js +36 -0
  32. package/plugins/index.js +2 -0
  33. package/src/file-router.js +21 -1
  34. package/templates/skills/webspresso-usage/REFERENCE-framework.md +1 -1
  35. package/templates/skills/webspresso-usage/SKILL.md +5 -0
@@ -114,8 +114,76 @@ const RichTextField = {
114
114
  }
115
115
  };
116
116
 
117
+ function isImageAccept(accept) {
118
+ return accept && accept !== '*/*' && String(accept).indexOf('image') !== -1;
119
+ }
120
+
121
+ function isImageUrl(url) {
122
+ if (!url || typeof url !== 'string') return false;
123
+ if (url.indexOf('blob:') === 0) return true;
124
+ return /\.(jpe?g|png|gif|webp|svg|avif|bmp|ico)(\?|#|$)/i.test(url.trim());
125
+ }
126
+
127
+ function shouldShowImagePreview(url, accept) {
128
+ return isImageAccept(accept) || isImageUrl(url);
129
+ }
130
+
131
+ function renderUploadedFilePreview(url, options) {
132
+ var accept = options.accept;
133
+ var readonly = options.readonly;
134
+ var onRemove = options.onRemove;
135
+ var label = options.label;
136
+ if (!url) return null;
137
+
138
+ if (shouldShowImagePreview(url, accept)) {
139
+ return m('.mt-3.flex.flex-col.items-start.gap-2', [
140
+ m('a.block', { href: url, target: '_blank', rel: 'noopener noreferrer' },
141
+ m('img.max-h-48.max-w-full.rounded-lg.border.border-gray-200.dark:border-slate-600.object-contain.bg-white.dark:bg-slate-900.shadow-sm', {
142
+ src: url,
143
+ alt: label || 'Preview',
144
+ loading: 'lazy',
145
+ })
146
+ ),
147
+ m('p.text-xs.text-gray-500.dark:text-slate-400.break-all', url),
148
+ !readonly && onRemove
149
+ ? m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-sm', {
150
+ type: 'button',
151
+ onclick: onRemove,
152
+ }, 'Remove')
153
+ : null,
154
+ ]);
155
+ }
156
+
157
+ return m('.mt-3.flex.flex-col.items-start.gap-2', [
158
+ m('a.text-sm.text-indigo-600.dark:text-indigo-400.break-all', {
159
+ href: url,
160
+ target: '_blank',
161
+ rel: 'noopener noreferrer',
162
+ }, url),
163
+ !readonly && onRemove
164
+ ? m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-sm', {
165
+ type: 'button',
166
+ onclick: onRemove,
167
+ }, 'Remove')
168
+ : null,
169
+ ]);
170
+ }
171
+
172
+ function revokeLocalPreview(state) {
173
+ if (state && state.localPreview) {
174
+ try { URL.revokeObjectURL(state.localPreview); } catch (e) {}
175
+ state.localPreview = null;
176
+ }
177
+ }
178
+
117
179
  // File upload field (multipart POST to settings.uploadUrl; field name "file")
118
180
  const FileUploadField = {
181
+ oninit: function (vnode) {
182
+ vnode.state.localPreview = null;
183
+ },
184
+ onremove: function (vnode) {
185
+ revokeLocalPreview(vnode.state);
186
+ },
119
187
  oncreate: (vnode) => {
120
188
  const col = vnode.attrs.col;
121
189
  const readonly = vnode.attrs.readonly;
@@ -128,6 +196,7 @@ const FileUploadField = {
128
196
  if (!dropZone) return;
129
197
  const meta = col.ui || {};
130
198
  const onChange = vnode.attrs.onChange;
199
+ const state = vnode.state;
131
200
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function (eventName) {
132
201
  dropZone.addEventListener(eventName, function (e) {
133
202
  e.preventDefault();
@@ -147,14 +216,14 @@ const FileUploadField = {
147
216
  dropZone.addEventListener('drop', function (e) {
148
217
  var files = e.dataTransfer.files;
149
218
  if (files.length > 0) {
150
- handleAdminFileUpload(files[0], onChange, meta);
219
+ handleAdminFileUpload(files[0], onChange, meta, state);
151
220
  }
152
221
  });
153
222
  var fileInput = dropZone.querySelector('input[type=file]');
154
223
  if (fileInput) {
155
224
  fileInput.addEventListener('change', function (e) {
156
225
  if (e.target.files.length > 0) {
157
- handleAdminFileUpload(e.target.files[0], onChange, meta);
226
+ handleAdminFileUpload(e.target.files[0], onChange, meta, state);
158
227
  }
159
228
  });
160
229
  }
@@ -164,6 +233,7 @@ const FileUploadField = {
164
233
  const value = vnode.attrs.value || '';
165
234
  const onChange = vnode.attrs.onChange;
166
235
  const readonly = vnode.attrs.readonly || false;
236
+ const state = vnode.state;
167
237
  const meta = col.ui || {};
168
238
  const label = meta.label || formatColumnLabel(col.name);
169
239
  const hint = meta.hint || '';
@@ -176,10 +246,17 @@ const FileUploadField = {
176
246
  var maxSize = meta.maxSize || meta.maxBytes || (10 * 1024 * 1024);
177
247
  var accept = meta.accept || '*/*';
178
248
  var dropZoneId = 'drop-zone-' + col.name;
249
+ var displayUrl = state.localPreview || value;
250
+ var clearValue = function () {
251
+ revokeLocalPreview(state);
252
+ if (onChange) onChange('');
253
+ };
179
254
  if (readonly) {
180
255
  return m('.mb-4', [
181
256
  m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', label, required ? m('span.text-red-500', ' *') : null),
182
- value ? m('a.text-indigo-600.dark:text-indigo-400.break-all', { href: value, target: '_blank', rel: 'noopener noreferrer' }, value) : m('span.text-gray-400.dark:text-slate-500', '—'),
257
+ value
258
+ ? renderUploadedFilePreview(value, { accept: accept, readonly: true, label: label })
259
+ : m('span.text-gray-400.dark:text-slate-500', '—'),
183
260
  hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
184
261
  ]);
185
262
  }
@@ -196,32 +273,29 @@ const FileUploadField = {
196
273
  required: required,
197
274
  oninput: function (e) { if (onChange) onChange(e.target.value); },
198
275
  }),
276
+ value ? renderUploadedFilePreview(value, { accept: accept, readonly: false, onRemove: clearValue, label: label }) : null,
199
277
  hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
200
278
  ]);
201
279
  }
202
280
  return m('.mb-4', [
203
281
  m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', label, required ? m('span.text-red-500', ' *') : null),
204
- m('div#' + dropZoneId + '.border-2.border-dashed.border-gray-300.dark:border-slate-600.rounded-lg.p-8.text-center.bg-gray-50.dark:bg-slate-900/40', { style: 'cursor: pointer;' }, [
282
+ displayUrl
283
+ ? renderUploadedFilePreview(displayUrl, { accept: accept, readonly: false, onRemove: clearValue, label: label })
284
+ : null,
285
+ m('div#' + dropZoneId + '.border-2.border-dashed.border-gray-300.dark:border-slate-600.rounded-lg.p-6.text-center.bg-gray-50.dark:bg-slate-900/40', { style: 'cursor: pointer;' }, [
205
286
  m('input[type=file].hidden', {
206
287
  id: 'file-input-' + col.name,
207
288
  accept: accept,
208
289
  onchange: function (e) {
209
290
  if (e.target.files.length > 0) {
210
- handleAdminFileUpload(e.target.files[0], onChange, meta);
291
+ handleAdminFileUpload(e.target.files[0], onChange, meta, state);
211
292
  }
212
293
  },
213
294
  }),
214
295
  m('div', [
215
- m('p.text-gray-600.dark:text-slate-400.mb-2', 'Drag and drop a file here, or'),
216
- m('label.text-blue-600.hover:text-blue-800.dark:text-blue-400.cursor-pointer', { for: 'file-input-' + col.name }, 'browse'),
296
+ m('p.text-gray-600.dark:text-slate-400.mb-2', displayUrl ? 'Drag and drop to replace, or' : 'Drag and drop a file here, or'),
297
+ m('label.text-blue-600.hover:text-blue-800.dark:text-blue-400.cursor-pointer', { for: 'file-input-' + col.name }, displayUrl ? 'choose another file' : 'browse'),
217
298
  ]),
218
- value ? m('.mt-4.text-left', [
219
- m('p.text-sm.text-gray-600.dark:text-slate-400.break-all', 'Current: ' + value),
220
- m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-sm.mt-2', {
221
- type: 'button',
222
- onclick: function () { if (onChange) onChange(''); },
223
- }, 'Remove'),
224
- ]) : null,
225
299
  ]),
226
300
  m('input[type=hidden]', { name: col.name, value: typeof value === 'string' ? value : '' }),
227
301
  m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', 'Max ' + Math.round(maxSize / 1024 / 1024) + ' MB (server enforces limits)'),
@@ -230,7 +304,7 @@ const FileUploadField = {
230
304
  },
231
305
  };
232
306
 
233
- async function handleAdminFileUpload(file, onChange, meta) {
307
+ async function handleAdminFileUpload(file, onChange, meta, state) {
234
308
  var uploadUrl = '';
235
309
  try {
236
310
  var cfg = window.__ADMIN_CONFIG__;
@@ -245,6 +319,11 @@ async function handleAdminFileUpload(file, onChange, meta) {
245
319
  alert('File too large (max ' + Math.round(maxSize / 1024 / 1024) + ' MB).');
246
320
  return;
247
321
  }
322
+ if (state && file.type && file.type.indexOf('image/') === 0) {
323
+ revokeLocalPreview(state);
324
+ state.localPreview = URL.createObjectURL(file);
325
+ m.redraw();
326
+ }
248
327
  var fd = new FormData();
249
328
  fd.append('file', file);
250
329
  try {
@@ -252,13 +331,18 @@ async function handleAdminFileUpload(file, onChange, meta) {
252
331
  var data = {};
253
332
  try { data = await res.json(); } catch (e2) { data = {}; }
254
333
  if (!res.ok) {
334
+ revokeLocalPreview(state);
335
+ m.redraw();
255
336
  alert(data.message || data.error || ('Upload failed (' + res.status + ')'));
256
337
  return;
257
338
  }
339
+ revokeLocalPreview(state);
258
340
  var url = data.url || data.publicUrl || '';
259
341
  if (onChange) onChange(url);
260
342
  m.redraw();
261
343
  } catch (err) {
344
+ revokeLocalPreview(state);
345
+ m.redraw();
262
346
  alert(err.message || 'Upload failed');
263
347
  }
264
348
  }
@@ -19,7 +19,7 @@ const LoginForm = {
19
19
  password: data.get('password'),
20
20
  });
21
21
  state.user = result.user;
22
- m.route.set('/');
22
+ m.route.set(consumeIntendedRoute('/'));
23
23
  } catch (err) {
24
24
  state.error = err.message;
25
25
  } finally {
@@ -78,7 +78,7 @@ const SetupForm = {
78
78
  });
79
79
  state.user = result.user;
80
80
  state.needsSetup = false;
81
- m.route.set('/');
81
+ m.route.set(consumeIntendedRoute('/'));
82
82
  } catch (err) {
83
83
  state.error = err.message;
84
84
  } finally {
@@ -13,8 +13,76 @@ function getUploadUrlFromAdminConfig() {
13
13
  }
14
14
  }
15
15
 
16
+ function isImageAccept(accept) {
17
+ return accept && accept !== '*/*' && String(accept).includes('image');
18
+ }
19
+
20
+ function isImageUrl(url) {
21
+ if (!url || typeof url !== 'string') return false;
22
+ if (url.startsWith('blob:')) return true;
23
+ return /\.(jpe?g|png|gif|webp|svg|avif|bmp|ico)(\?|#|$)/i.test(url.trim());
24
+ }
25
+
26
+ function shouldShowImagePreview(url, accept) {
27
+ return isImageAccept(accept) || isImageUrl(url);
28
+ }
29
+
30
+ function renderUploadedFilePreview(url, { accept, readonly, onRemove, label }) {
31
+ if (!url) return null;
32
+
33
+ if (shouldShowImagePreview(url, accept)) {
34
+ return m('.mt-3.flex.flex-col.items-start.gap-2', [
35
+ m('a.block', { href: url, target: '_blank', rel: 'noopener noreferrer' },
36
+ m('img.max-h-48.max-w-full.rounded-lg.border.border-gray-200.dark:border-slate-600.object-contain.bg-white.dark:bg-slate-900.shadow-sm', {
37
+ src: url,
38
+ alt: label || 'Preview',
39
+ loading: 'lazy',
40
+ })
41
+ ),
42
+ m('p.text-xs.text-gray-500.dark:text-slate-400.break-all', url),
43
+ !readonly && onRemove
44
+ ? m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-sm', {
45
+ type: 'button',
46
+ onclick: onRemove,
47
+ }, 'Remove')
48
+ : null,
49
+ ]);
50
+ }
51
+
52
+ return m('.mt-3.flex.flex-col.items-start.gap-2', [
53
+ m('a.text-sm.text-indigo-600.dark:text-indigo-400.break-all', {
54
+ href: url,
55
+ target: '_blank',
56
+ rel: 'noopener noreferrer',
57
+ }, url),
58
+ !readonly && onRemove
59
+ ? m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-sm', {
60
+ type: 'button',
61
+ onclick: onRemove,
62
+ }, 'Remove')
63
+ : null,
64
+ ]);
65
+ }
66
+
67
+ function revokeLocalPreview(state) {
68
+ if (state?.localPreview) {
69
+ try {
70
+ URL.revokeObjectURL(state.localPreview);
71
+ } catch {
72
+ // ignore
73
+ }
74
+ state.localPreview = null;
75
+ }
76
+ }
77
+
16
78
  module.exports = {
17
79
  FileUploadField: {
80
+ oninit: (vnode) => {
81
+ vnode.state.localPreview = null;
82
+ },
83
+ onremove: (vnode) => {
84
+ revokeLocalPreview(vnode.state);
85
+ },
18
86
  oncreate: (vnode) => {
19
87
  const { name, meta = {} } = vnode.attrs;
20
88
  if (!getUploadUrlFromAdminConfig()) return;
@@ -60,17 +128,24 @@ module.exports = {
60
128
  },
61
129
 
62
130
  view: (vnode) => {
63
- const { name, value = '', meta = {}, required = false } = vnode.attrs;
131
+ const { name, value = '', meta = {}, required = false, readonly = false } = vnode.attrs;
132
+ const state = vnode.state;
64
133
  const dropZoneId = 'drop-zone-' + name;
65
134
  const maxSize = meta.maxSize || meta.maxBytes || 10 * 1024 * 1024;
66
135
  const accept = meta.accept || '*/*';
67
136
  const uploadUrl = getUploadUrlFromAdminConfig();
137
+ const displayUrl = state.localPreview || value;
138
+ const label = meta.label || name;
139
+ const clearValue = () => {
140
+ revokeLocalPreview(state);
141
+ if (vnode.attrs.onchange) vnode.attrs.onchange('');
142
+ };
68
143
 
69
144
  if (!uploadUrl) {
70
145
  return m('.mb-4', [
71
146
  m(
72
147
  'label.block.text-sm.font-medium.mb-2',
73
- meta.label || name,
148
+ label,
74
149
  required ? m('span.text-red-500', ' *') : null
75
150
  ),
76
151
  m('p.text-xs.text-amber-700.dark:text-amber-400.mb-2', 'Upload URL is not configured.'),
@@ -80,62 +155,57 @@ module.exports = {
80
155
  value: typeof value === 'string' ? value : '',
81
156
  placeholder: 'https://… or /uploads/…',
82
157
  required,
158
+ readonly,
83
159
  oninput: (e) => {
84
160
  if (vnode.attrs.onchange) vnode.attrs.onchange(e.target.value);
85
161
  },
86
162
  }),
163
+ value
164
+ ? renderUploadedFilePreview(value, { accept, readonly, onRemove: clearValue, label })
165
+ : null,
87
166
  ]);
88
167
  }
89
168
 
90
169
  return m('.mb-4', [
91
170
  m(
92
171
  'label.block.text-sm.font-medium.mb-2',
93
- meta.label || name,
172
+ label,
94
173
  required ? m('span.text-red-500', ' *') : null
95
174
  ),
175
+ displayUrl
176
+ ? renderUploadedFilePreview(displayUrl, { accept, readonly, onRemove: clearValue, label })
177
+ : null,
96
178
  m(
97
- 'div.border-2.border-dashed.border-gray-300.dark:border-slate-600.rounded.p-8.text-center',
179
+ 'div.border-2.border-dashed.border-gray-300.dark:border-slate-600.rounded.p-6.text-center',
98
180
  {
99
181
  id: dropZoneId,
100
- style: 'cursor: pointer;',
182
+ style: readonly ? undefined : 'cursor: pointer;',
101
183
  },
102
184
  [
103
185
  m('input[type=file]', {
104
186
  class: 'hidden',
105
187
  id: 'file-input-' + name,
106
188
  accept,
189
+ disabled: readonly,
107
190
  onchange: (e) => {
108
191
  if (e.target.files.length > 0) {
109
192
  handleFileUpload(e.target.files[0], vnode, meta);
110
193
  }
111
194
  },
112
195
  }),
113
- m('div', [
114
- m('p.text-gray-600.dark:text-slate-400.mb-2', 'Drag and drop a file here, or'),
115
- m(
116
- 'label.text-blue-600.hover:text-blue-800.cursor-pointer',
117
- { for: 'file-input-' + name },
118
- 'browse'
119
- ),
120
- ]),
121
- value
122
- ? m('.mt-4', [
196
+ readonly
197
+ ? null
198
+ : m('div', [
123
199
  m(
124
- 'p.text-sm.text-gray-600.break-all',
125
- 'Current: ' + (typeof value === 'string' ? value : value.name || 'uploaded')
200
+ 'p.text-gray-600.dark:text-slate-400.mb-2',
201
+ displayUrl ? 'Drag and drop to replace, or' : 'Drag and drop a file here, or'
126
202
  ),
127
203
  m(
128
- 'button.text-red-600.hover:text-red-800.text-sm.mt-2',
129
- {
130
- type: 'button',
131
- onclick: () => {
132
- if (vnode.attrs.onchange) vnode.attrs.onchange('');
133
- },
134
- },
135
- 'Remove'
204
+ 'label.text-blue-600.hover:text-blue-800.cursor-pointer',
205
+ { for: 'file-input-' + name },
206
+ displayUrl ? 'choose another file' : 'browse'
136
207
  ),
137
- ])
138
- : null,
208
+ ]),
139
209
  ]
140
210
  ),
141
211
  m('input[type=hidden]', {
@@ -169,6 +239,12 @@ async function handleFileUpload(file, vnode, meta) {
169
239
  return;
170
240
  }
171
241
 
242
+ if (file.type?.startsWith('image/')) {
243
+ revokeLocalPreview(vnode.state);
244
+ vnode.state.localPreview = URL.createObjectURL(file);
245
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
246
+ }
247
+
172
248
  const fd = new FormData();
173
249
  fd.append('file', file);
174
250
 
@@ -181,13 +257,18 @@ async function handleFileUpload(file, vnode, meta) {
181
257
  data = {};
182
258
  }
183
259
  if (!res.ok) {
260
+ revokeLocalPreview(vnode.state);
261
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
184
262
  alert(data.message || data.error || `Upload failed (${res.status})`);
185
263
  return;
186
264
  }
265
+ revokeLocalPreview(vnode.state);
187
266
  const url = data.url || data.publicUrl || '';
188
267
  if (vnode.attrs.onchange) vnode.attrs.onchange(url);
189
268
  if (typeof m !== 'undefined' && m.redraw) m.redraw();
190
269
  } catch (err) {
270
+ revokeLocalPreview(vnode.state);
271
+ if (typeof m !== 'undefined' && m.redraw) m.redraw();
191
272
  alert(err.message || 'Upload failed');
192
273
  }
193
274
  }
@@ -119,6 +119,22 @@ function adminPanelPlugin(options = {}) {
119
119
  if (typeof configure === 'function') {
120
120
  configure(registry);
121
121
  }
122
+
123
+ // Session must be registered before file routes so SSR pages can read adminUser.
124
+ if (enabled && ctx.app && !ctx.app._webspressoSessionInitialized) {
125
+ const secret = sessionSecret || process.env.SESSION_SECRET || 'webspresso-admin-secret-change-in-production';
126
+ ctx.app.use(session({
127
+ secret,
128
+ resave: false,
129
+ saveUninitialized: false,
130
+ cookie: {
131
+ secure: process.env.NODE_ENV === 'production',
132
+ httpOnly: true,
133
+ maxAge: 24 * 60 * 60 * 1000, // 24 hours
134
+ },
135
+ }));
136
+ ctx.app._webspressoSessionInitialized = true;
137
+ }
122
138
  },
123
139
 
124
140
  /**
@@ -164,24 +180,7 @@ function adminPanelPlugin(options = {}) {
164
180
  db.registerModel(AdminUser);
165
181
  }
166
182
 
167
- // Setup session middleware (only once)
168
- if (!app._webspressoSessionInitialized) {
169
- const secret = sessionSecret || process.env.SESSION_SECRET || 'webspresso-admin-secret-change-in-production';
170
-
171
- app.use(session({
172
- secret,
173
- resave: false,
174
- saveUninitialized: false,
175
- cookie: {
176
- secure: process.env.NODE_ENV === 'production',
177
- httpOnly: true,
178
- maxAge: 24 * 60 * 60 * 1000, // 24 hours
179
- },
180
- }));
181
-
182
- app._webspressoSessionInitialized = true;
183
- }
184
-
183
+ // Session middleware is initialized in register() (before file routes).
185
184
  // Register default modules
186
185
  registerSystemMenuItems({ registry });
187
186
  registerModelMenuItems({ registry, db });
@@ -351,6 +351,7 @@ const Sidebar = {
351
351
  await api.post('/auth/logout');
352
352
  state.user = null;
353
353
  sidebarOpen = false;
354
+ clearIntendedRoute();
354
355
  m.route.set('/login');
355
356
  },
356
357
  }, m(Icon, { name: 'logout', class: 'w-5 h-5' })),