@jant/core 0.2.18 → 0.2.19

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.
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
4
4
  */ import { Hono } from "hono";
5
5
  import { useLingui as $_useLingui } from "@jant/core/i18n";
6
6
  import { DashLayout } from "../../theme/layouts/index.js";
7
- import { sse } from "../../lib/sse.js";
7
+ import { dsRedirect, dsToast } from "../../lib/sse.js";
8
8
  import { getSiteName } from "../../lib/config.js";
9
9
  import { SETTINGS_KEYS } from "../../lib/constants.js";
10
10
  import { getAvailableThemes } from "../../lib/theme.js";
@@ -148,9 +148,7 @@ appearanceRoutes.post("/", async (c)=>{
148
148
  // Validate theme ID
149
149
  const validTheme = themes.find((t)=>t.id === body.theme);
150
150
  if (!validTheme) {
151
- return sse(c, async (stream)=>{
152
- await stream.toast("Invalid theme selected.", "error");
153
- });
151
+ return dsToast("Invalid theme selected.", "error");
154
152
  }
155
153
  if (validTheme.id === "default") {
156
154
  await settings.remove(SETTINGS_KEYS.THEME);
@@ -158,7 +156,5 @@ appearanceRoutes.post("/", async (c)=>{
158
156
  await settings.set(SETTINGS_KEYS.THEME, validTheme.id);
159
157
  }
160
158
  // Full page reload to apply the new theme CSS
161
- return sse(c, async (stream)=>{
162
- await stream.redirect("/dash/appearance?saved");
163
- });
159
+ return dsRedirect("/dash/appearance?saved");
164
160
  });
@@ -7,7 +7,7 @@ import { useLingui as $_useLingui } from "@jant/core/i18n";
7
7
  import { DashLayout } from "../../theme/layouts/index.js";
8
8
  import { EmptyState, ListItemRow, ActionButtons, CrudPageHeader, DangerZone } from "../../theme/components/index.js";
9
9
  import * as sqid from "../../lib/sqid.js";
10
- import { sse } from "../../lib/sse.js";
10
+ import { dsRedirect } from "../../lib/sse.js";
11
11
  export const collectionsRoutes = new Hono();
12
12
  function CollectionsListContent({ collections }) {
13
13
  const { i18n: $__i18n, _: $__ } = $_useLingui();
@@ -428,9 +428,7 @@ collectionsRoutes.post("/", async (c)=>{
428
428
  path: body.path,
429
429
  description: body.description || undefined
430
430
  });
431
- return sse(c, async (stream)=>{
432
- await stream.redirect(`/dash/collections/${collection.id}`);
433
- });
431
+ return dsRedirect(`/dash/collections/${collection.id}`);
434
432
  });
435
433
  // View single collection
436
434
  collectionsRoutes.get("/:id", async (c)=>{
@@ -478,18 +476,14 @@ collectionsRoutes.post("/:id", async (c)=>{
478
476
  path: body.path,
479
477
  description: body.description || undefined
480
478
  });
481
- return sse(c, async (stream)=>{
482
- await stream.redirect(`/dash/collections/${id}`);
483
- });
479
+ return dsRedirect(`/dash/collections/${id}`);
484
480
  });
485
481
  // Delete collection
486
482
  collectionsRoutes.post("/:id/delete", async (c)=>{
487
483
  const id = parseInt(c.req.param("id"), 10);
488
484
  if (isNaN(id)) return c.notFound();
489
485
  await c.var.services.collections.delete(id);
490
- return sse(c, async (stream)=>{
491
- await stream.redirect("/dash/collections");
492
- });
486
+ return dsRedirect("/dash/collections");
493
487
  });
494
488
  // Remove post from collection
495
489
  collectionsRoutes.post("/:id/remove-post", async (c)=>{
@@ -499,7 +493,5 @@ collectionsRoutes.post("/:id/remove-post", async (c)=>{
499
493
  if (body.postId) {
500
494
  await c.var.services.collections.removePost(id, body.postId);
501
495
  }
502
- return sse(c, async (stream)=>{
503
- await stream.redirect(`/dash/collections/${id}`);
504
- });
496
+ return dsRedirect(`/dash/collections/${id}`);
505
497
  });
@@ -11,7 +11,7 @@ import { DashLayout } from "../../theme/layouts/index.js";
11
11
  import { EmptyState, DangerZone } from "../../theme/components/index.js";
12
12
  import * as time from "../../lib/time.js";
13
13
  import { getMediaUrl, getImageUrl } from "../../lib/image.js";
14
- import { sse } from "../../lib/sse.js";
14
+ import { dsRedirect } from "../../lib/sse.js";
15
15
  export const mediaRoutes = new Hono();
16
16
  /**
17
17
  * Format file size for display
@@ -72,8 +72,7 @@ export const mediaRoutes = new Hono();
72
72
  /**
73
73
  * Media list page content
74
74
  *
75
- * Uses plain JavaScript for upload state management (more reliable than Datastar signals
76
- * for complex async flows like file uploads with SSE responses).
75
+ * Upload is handled by media-upload.ts (client module) + Datastar @post for SSE.
77
76
  */ function MediaListContent({ mediaList, r2PublicUrl, imageTransformUrl }) {
78
77
  const { i18n: $__i18n, _: $__ } = $_useLingui();
79
78
  const processingText = $__i18n._({
@@ -92,164 +91,18 @@ export const mediaRoutes = new Hono();
92
91
  id: "pZq3aX",
93
92
  message: "Upload failed. Please try again."
94
93
  });
95
- // Plain JavaScript upload handler - shows progress in the list
96
- const uploadScript = `
97
- async function handleMediaUpload(input) {
98
- if (!input.files || !input.files[0]) return;
99
-
100
- const file = input.files[0];
101
- const errorBox = document.getElementById('upload-error');
102
- errorBox.classList.add('hidden');
103
-
104
- // Ensure grid exists (remove empty state if needed)
105
- let grid = document.getElementById('media-grid');
106
- if (!grid) {
107
- document.getElementById('empty-state')?.remove();
108
- grid = document.createElement('div');
109
- grid.id = 'media-grid';
110
- grid.className = 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4';
111
- document.getElementById('media-content').appendChild(grid);
112
- }
113
-
114
- // Create placeholder card showing progress
115
- const placeholder = document.createElement('div');
116
- placeholder.id = 'upload-placeholder';
117
- placeholder.className = 'group relative';
118
- placeholder.innerHTML = \`
119
- <div class="aspect-square bg-muted rounded-lg overflow-hidden border flex items-center justify-center">
120
- <div class="text-center px-2">
121
- <svg class="animate-spin h-6 w-6 text-muted-foreground mx-auto mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
122
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
123
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
124
- </svg>
125
- <span id="upload-status" class="text-xs text-muted-foreground">${processingText}</span>
126
- </div>
127
- </div>
128
- <div class="mt-2 text-xs truncate" title="\${file.name}">\${file.name}</div>
129
- <div class="text-xs text-muted-foreground">\${formatFileSize(file.size)}</div>
130
- \`;
131
- grid.prepend(placeholder);
132
-
133
- try {
134
- if (typeof ImageProcessor === 'undefined') {
135
- throw new Error('ImageProcessor not loaded');
136
- }
137
-
138
- // Process image client-side
139
- const processed = await ImageProcessor.processToFile(file);
140
- document.getElementById('upload-status').textContent = '${uploadingText}';
141
-
142
- // Upload with SSE response
143
- const fd = new FormData();
144
- fd.append('file', processed);
145
-
146
- const response = await fetch('/api/upload', {
147
- method: 'POST',
148
- body: fd,
149
- headers: { 'Accept': 'text/event-stream' }
150
- });
151
-
152
- if (!response.ok) throw new Error('Upload failed: ' + response.status);
153
-
154
- // Parse SSE stream - will replace placeholder with real card
155
- const reader = response.body.getReader();
156
- const decoder = new TextDecoder();
157
- let buffer = '';
158
-
159
- while (true) {
160
- const { done, value } = await reader.read();
161
- if (done) break;
162
-
163
- buffer += decoder.decode(value, { stream: true });
164
- const events = buffer.split('\\n\\n');
165
- buffer = events.pop() || '';
166
-
167
- for (const event of events) {
168
- if (!event.trim()) continue;
169
- processSSEEvent(event);
170
- }
171
- }
172
-
173
- } catch (err) {
174
- console.error('Upload error:', err);
175
- // Show error in placeholder
176
- placeholder.innerHTML = \`
177
- <div class="aspect-square bg-destructive/10 rounded-lg overflow-hidden border border-destructive flex items-center justify-center">
178
- <div class="text-center px-2">
179
- <span class="text-xs text-destructive">\${err.message || '${errorText}'}</span>
180
- </div>
181
- </div>
182
- <div class="mt-2 text-xs truncate text-destructive">\${file.name}</div>
183
- <button type="button" class="text-xs text-muted-foreground hover:underline" onclick="this.closest('.group').remove()">Remove</button>
184
- \`;
185
- }
186
-
187
- input.value = '';
188
- }
189
-
190
- function formatFileSize(bytes) {
191
- if (bytes < 1024) return bytes + ' B';
192
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
193
- return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
194
- }
195
-
196
- function processSSEEvent(event) {
197
- const lines = event.split('\\n');
198
- let eventType = '';
199
- const data = {};
200
- let elementsLines = [];
201
- let inElements = false;
202
-
203
- for (const line of lines) {
204
- if (line.startsWith('event: ')) {
205
- eventType = line.slice(7);
206
- } else if (line.startsWith('data: ')) {
207
- const content = line.slice(6);
208
- if (content.startsWith('mode ')) {
209
- data.mode = content.slice(5);
210
- inElements = false;
211
- } else if (content.startsWith('selector ')) {
212
- data.selector = content.slice(9);
213
- inElements = false;
214
- } else if (content.startsWith('elements ')) {
215
- elementsLines = [content.slice(9)];
216
- inElements = true;
217
- } else if (inElements) {
218
- // Continuation of elements content
219
- elementsLines.push(content);
220
- }
221
- }
222
- }
223
-
224
- if (elementsLines.length > 0) {
225
- data.elements = elementsLines.join('\\n');
226
- }
227
-
228
- if (eventType === 'datastar-patch-elements') {
229
- if (data.mode === 'remove' && data.selector) {
230
- document.querySelector(data.selector)?.remove();
231
- } else if (data.mode === 'outer' && data.selector && data.elements) {
232
- // Replace element entirely (used for placeholder -> real card)
233
- const target = document.querySelector(data.selector);
234
- if (target) {
235
- const temp = document.createElement('div');
236
- temp.innerHTML = data.elements;
237
- const newElement = temp.firstElementChild;
238
- if (newElement) {
239
- target.replaceWith(newElement);
240
- if (window.Datastar) Datastar.apply(newElement);
241
- }
242
- }
243
- }
244
- }
245
- }
246
- `.trim();
247
94
  return /*#__PURE__*/ _jsxs(_Fragment, {
248
95
  children: [
249
- /*#__PURE__*/ _jsx("script", {
250
- dangerouslySetInnerHTML: {
251
- __html: uploadScript
252
- }
96
+ /*#__PURE__*/ _jsx("form", {
97
+ id: "upload-form",
98
+ class: "hidden",
99
+ enctype: "multipart/form-data",
100
+ "data-on:submit__prevent": "@post('/api/upload', {contentType: 'form'})",
101
+ children: /*#__PURE__*/ _jsx("input", {
102
+ id: "upload-file-input",
103
+ type: "file",
104
+ name: "file"
105
+ })
253
106
  }),
254
107
  /*#__PURE__*/ _jsxs("div", {
255
108
  class: "flex items-center justify-between mb-6",
@@ -271,16 +124,15 @@ function processSSEEvent(event) {
271
124
  type: "file",
272
125
  class: "hidden",
273
126
  accept: "image/*",
274
- onchange: "handleMediaUpload(this)"
127
+ "data-media-upload": true,
128
+ "data-text-processing": processingText,
129
+ "data-text-uploading": uploadingText,
130
+ "data-text-error": errorText
275
131
  })
276
132
  ]
277
133
  })
278
134
  ]
279
135
  }),
280
- /*#__PURE__*/ _jsx("div", {
281
- id: "upload-error",
282
- class: "hidden"
283
- }),
284
136
  /*#__PURE__*/ _jsx("div", {
285
137
  class: "card mb-6",
286
138
  children: /*#__PURE__*/ _jsx("section", {
@@ -582,7 +434,5 @@ mediaRoutes.post("/:id/delete", async (c)=>{
582
434
  }
583
435
  // Delete from database
584
436
  await c.var.services.media.delete(id);
585
- return sse(c, async (stream)=>{
586
- await stream.redirect("/dash/media");
587
- });
437
+ return dsRedirect("/dash/media");
588
438
  });
@@ -10,7 +10,7 @@ import { DashLayout } from "../../theme/layouts/index.js";
10
10
  import { PageForm, VisibilityBadge, EmptyState, ListItemRow, ActionButtons, CrudPageHeader, DangerZone } from "../../theme/components/index.js";
11
11
  import * as sqid from "../../lib/sqid.js";
12
12
  import * as time from "../../lib/time.js";
13
- import { sse } from "../../lib/sse.js";
13
+ import { dsRedirect } from "../../lib/sse.js";
14
14
  export const pagesRoutes = new Hono();
15
15
  function PagesListContent({ pages }) {
16
16
  const { i18n: $__i18n, _: $__ } = $_useLingui();
@@ -224,9 +224,7 @@ pagesRoutes.post("/", async (c)=>{
224
224
  visibility: body.visibility,
225
225
  path: body.path.toLowerCase().replace(/[^a-z0-9-]/g, "-")
226
226
  });
227
- return sse(c, async (stream)=>{
228
- await stream.redirect(`/dash/pages/${sqid.encode(page.id)}`);
229
- });
227
+ return dsRedirect(`/dash/pages/${sqid.encode(page.id)}`);
230
228
  });
231
229
  // View single page
232
230
  pagesRoutes.get("/:id", async (c)=>{
@@ -274,16 +272,12 @@ pagesRoutes.post("/:id", async (c)=>{
274
272
  visibility: body.visibility,
275
273
  path: body.path.toLowerCase().replace(/[^a-z0-9-]/g, "-")
276
274
  });
277
- return sse(c, async (stream)=>{
278
- await stream.redirect(`/dash/pages/${sqid.encode(id)}`);
279
- });
275
+ return dsRedirect(`/dash/pages/${sqid.encode(id)}`);
280
276
  });
281
277
  // Delete page
282
278
  pagesRoutes.post("/:id/delete", async (c)=>{
283
279
  const id = sqid.decode(c.req.param("id"));
284
280
  if (!id) return c.notFound();
285
281
  await c.var.services.posts.delete(id);
286
- return sse(c, async (stream)=>{
287
- await stream.redirect("/dash/pages");
288
- });
282
+ return dsRedirect("/dash/pages");
289
283
  });
@@ -7,7 +7,7 @@ import { useLingui as $_useLingui } from "@jant/core/i18n";
7
7
  import { DashLayout } from "../../theme/layouts/index.js";
8
8
  import { PostForm, PostList, CrudPageHeader, ActionButtons } from "../../theme/components/index.js";
9
9
  import * as sqid from "../../lib/sqid.js";
10
- import { sse } from "../../lib/sse.js";
10
+ import { dsRedirect } from "../../lib/sse.js";
11
11
  export const postsRoutes = new Hono();
12
12
  function PostsListContent({ posts }) {
13
13
  const { i18n: $__i18n, _: $__ } = $_useLingui();
@@ -90,9 +90,7 @@ postsRoutes.post("/", async (c)=>{
90
90
  sourceUrl: body.sourceUrl || undefined,
91
91
  path: body.path || undefined
92
92
  });
93
- return sse(c, async (stream)=>{
94
- await stream.redirect(`/dash/posts/${sqid.encode(post.id)}`);
95
- });
93
+ return dsRedirect(`/dash/posts/${sqid.encode(post.id)}`);
96
94
  });
97
95
  function ViewPostContent({ post }) {
98
96
  const { i18n: $__i18n, _: $__ } = $_useLingui();
@@ -203,16 +201,12 @@ postsRoutes.post("/:id", async (c)=>{
203
201
  sourceUrl: body.sourceUrl || null,
204
202
  path: body.path || null
205
203
  });
206
- return sse(c, async (stream)=>{
207
- await stream.redirect(`/dash/posts/${sqid.encode(id)}`);
208
- });
204
+ return dsRedirect(`/dash/posts/${sqid.encode(id)}`);
209
205
  });
210
206
  // Delete post
211
207
  postsRoutes.post("/:id/delete", async (c)=>{
212
208
  const id = sqid.decode(c.req.param("id"));
213
209
  if (!id) return c.notFound();
214
210
  await c.var.services.posts.delete(id);
215
- return sse(c, async (stream)=>{
216
- await stream.redirect("/dash/posts");
217
- });
211
+ return dsRedirect("/dash/posts");
218
212
  });
@@ -6,7 +6,7 @@ import { getSiteName } from "../../lib/config.js";
6
6
  import { useLingui as $_useLingui } from "@jant/core/i18n";
7
7
  import { DashLayout } from "../../theme/layouts/index.js";
8
8
  import { EmptyState, ListItemRow, ActionButtons, CrudPageHeader } from "../../theme/components/index.js";
9
- import { sse } from "../../lib/sse.js";
9
+ import { dsRedirect } from "../../lib/sse.js";
10
10
  export const redirectsRoutes = new Hono();
11
11
  function RedirectsListContent({ redirects }) {
12
12
  const { i18n: $__i18n, _: $__ } = $_useLingui();
@@ -225,9 +225,7 @@ redirectsRoutes.post("/", async (c)=>{
225
225
  const body = await c.req.json();
226
226
  const type = parseInt(body.type, 10);
227
227
  await c.var.services.redirects.create(body.fromPath, body.toPath, type);
228
- return sse(c, async (stream)=>{
229
- await stream.redirect("/dash/redirects");
230
- });
228
+ return dsRedirect("/dash/redirects");
231
229
  });
232
230
  // Delete redirect
233
231
  redirectsRoutes.post("/:id/delete", async (c)=>{
@@ -235,7 +233,5 @@ redirectsRoutes.post("/:id/delete", async (c)=>{
235
233
  if (!isNaN(id)) {
236
234
  await c.var.services.redirects.delete(id);
237
235
  }
238
- return sse(c, async (stream)=>{
239
- await stream.redirect("/dash/redirects");
240
- });
236
+ return dsRedirect("/dash/redirects");
241
237
  });
@@ -1 +1 @@
1
- {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../src/routes/dash/settings.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAKjD,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D,eAAO,MAAM,cAAc,kDAAkB,CAAC"}
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../src/routes/dash/settings.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAcjD,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D,eAAO,MAAM,cAAc,kDAAkB,CAAC"}
@@ -4,8 +4,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-
4
4
  */ import { Hono } from "hono";
5
5
  import { useLingui as $_useLingui } from "@jant/core/i18n";
6
6
  import { DashLayout } from "../../theme/layouts/index.js";
7
- import { sse } from "../../lib/sse.js";
7
+ import { sse, dsToast } from "../../lib/sse.js";
8
8
  import { getSiteLanguage, getConfigFallback } from "../../lib/config.js";
9
+ /** Escape HTML special characters for safe insertion into HTML strings */ function escapeHtml(str) {
10
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
11
+ }
9
12
  export const settingsRoutes = new Hono();
10
13
  function SettingsContent({ siteName, siteDescription, siteLanguage, siteNameFallback, siteDescriptionFallback }) {
11
14
  const { i18n: $__i18n, _: $__ } = $_useLingui();
@@ -271,11 +274,21 @@ settingsRoutes.post("/", async (c)=>{
271
274
  // Language always has a value from the select
272
275
  await settings.set("SITE_LANGUAGE", body.siteLanguage);
273
276
  const languageChanged = oldLanguage !== body.siteLanguage;
277
+ // Determine the effective display name after save
278
+ const displayName = body.siteName.trim() || getConfigFallback(c, "SITE_NAME");
274
279
  return sse(c, async (stream)=>{
275
280
  if (languageChanged) {
276
281
  // Language changed - full reload needed to update all UI text
277
282
  await stream.redirect("/dash/settings?saved");
278
283
  } else {
284
+ const escaped = escapeHtml(displayName);
285
+ // Update header site name
286
+ await stream.patchElements(`<a id="site-name" href="/dash" class="font-semibold">${escaped}</a>`);
287
+ // Update page title
288
+ await stream.patchElements(`Settings - ${escaped}`, {
289
+ mode: "inner",
290
+ selector: "title"
291
+ });
279
292
  await stream.toast("Settings saved successfully.");
280
293
  }
281
294
  });
@@ -284,9 +297,7 @@ settingsRoutes.post("/", async (c)=>{
284
297
  settingsRoutes.post("/password", async (c)=>{
285
298
  const body = await c.req.json();
286
299
  if (body.newPassword !== body.confirmPassword) {
287
- return sse(c, async (stream)=>{
288
- await stream.toast("Passwords do not match.", "error");
289
- });
300
+ return dsToast("Passwords do not match.", "error");
290
301
  }
291
302
  try {
292
303
  await c.var.auth.api.changePassword({
@@ -298,9 +309,7 @@ settingsRoutes.post("/password", async (c)=>{
298
309
  headers: c.req.raw.headers
299
310
  });
300
311
  } catch {
301
- return sse(c, async (stream)=>{
302
- await stream.toast("Current password is incorrect.", "error");
303
- });
312
+ return dsToast("Current password is incorrect.", "error");
304
313
  }
305
314
  return sse(c, async (stream)=>{
306
315
  await stream.toast("Password changed successfully.");
@@ -22,6 +22,7 @@ function DashLayoutContent({ siteName, currentPath, children }) {
22
22
  class: "container flex h-14 items-center justify-between",
23
23
  children: [
24
24
  /*#__PURE__*/ _jsx("a", {
25
+ id: "site-name",
25
26
  href: "/dash",
26
27
  class: "font-semibold",
27
28
  children: siteName
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.2.18",
3
+ "version": "0.2.19",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "bin": {
package/src/app.tsx CHANGED
@@ -45,7 +45,7 @@ import { requireAuth } from "./middleware/auth.js";
45
45
 
46
46
  // Layouts for auth pages
47
47
  import { BaseLayout } from "./theme/layouts/index.js";
48
- import { sse } from "./lib/sse.js";
48
+ import { dsRedirect, dsToast } from "./lib/sse.js";
49
49
  import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
50
50
 
51
51
  // Extend Hono's context variables
@@ -275,21 +275,15 @@ export function createApp(config: JantConfig = {}): App {
275
275
  const { name, email, password } = body;
276
276
 
277
277
  if (!name || !email || !password) {
278
- return sse(c, async (stream) => {
279
- await stream.toast("All fields are required", "error");
280
- });
278
+ return dsToast("All fields are required", "error");
281
279
  }
282
280
 
283
281
  if (password.length < 8) {
284
- return sse(c, async (stream) => {
285
- await stream.toast("Password must be at least 8 characters", "error");
286
- });
282
+ return dsToast("Password must be at least 8 characters", "error");
287
283
  }
288
284
 
289
285
  if (!c.var.auth) {
290
- return sse(c, async (stream) => {
291
- await stream.toast("AUTH_SECRET not configured", "error");
292
- });
286
+ return dsToast("AUTH_SECRET not configured", "error");
293
287
  }
294
288
 
295
289
  try {
@@ -298,22 +292,16 @@ export function createApp(config: JantConfig = {}): App {
298
292
  });
299
293
 
300
294
  if (!signUpResponse || "error" in signUpResponse) {
301
- return sse(c, async (stream) => {
302
- await stream.toast("Failed to create account", "error");
303
- });
295
+ return dsToast("Failed to create account", "error");
304
296
  }
305
297
 
306
298
  await c.var.services.settings.completeOnboarding();
307
299
 
308
- return sse(c, async (stream) => {
309
- await stream.redirect("/signin?setup");
310
- });
300
+ return dsRedirect("/signin?setup");
311
301
  } catch (err) {
312
302
  // eslint-disable-next-line no-console -- Error logging is intentional
313
303
  console.error("Setup error:", err);
314
- return sse(c, async (stream) => {
315
- await stream.toast("Failed to create account", "error");
316
- });
304
+ return dsToast("Failed to create account", "error");
317
305
  }
318
306
  });
319
307
 
@@ -413,9 +401,7 @@ export function createApp(config: JantConfig = {}): App {
413
401
 
414
402
  app.post("/signin", async (c) => {
415
403
  if (!c.var.auth) {
416
- return sse(c, async (stream) => {
417
- await stream.toast("Auth not configured", "error");
418
- });
404
+ return dsToast("Auth not configured", "error");
419
405
  }
420
406
 
421
407
  const body = await c.req.json<{ email: string; password: string }>();
@@ -434,9 +420,7 @@ export function createApp(config: JantConfig = {}): App {
434
420
  const response = await c.var.auth.handler(signInRequest);
435
421
 
436
422
  if (!response.ok) {
437
- return sse(c, async (stream) => {
438
- await stream.toast("Invalid email or password", "error");
439
- });
423
+ return dsToast("Invalid email or password", "error");
440
424
  }
441
425
 
442
426
  // Forward Set-Cookie headers from auth response
@@ -446,19 +430,11 @@ export function createApp(config: JantConfig = {}): App {
446
430
  cookieHeaders["Set-Cookie"] = setCookie;
447
431
  }
448
432
 
449
- return sse(
450
- c,
451
- async (stream) => {
452
- await stream.redirect("/dash");
453
- },
454
- { headers: cookieHeaders },
455
- );
433
+ return dsRedirect("/dash", { headers: cookieHeaders });
456
434
  } catch (err) {
457
435
  // eslint-disable-next-line no-console -- Error logging is intentional
458
436
  console.error("Signin error:", err);
459
- return sse(c, async (stream) => {
460
- await stream.toast("Invalid email or password", "error");
461
- });
437
+ return dsToast("Invalid email or password", "error");
462
438
  }
463
439
  });
464
440
 
@@ -632,9 +608,7 @@ export function createApp(config: JantConfig = {}): App {
632
608
  SETTINGS_KEYS.PASSWORD_RESET_TOKEN,
633
609
  );
634
610
  if (!stored) {
635
- return sse(c, async (stream) => {
636
- await stream.toast("Invalid or expired reset link.", "error");
637
- });
611
+ return dsToast("Invalid or expired reset link.", "error");
638
612
  }
639
613
 
640
614
  const separatorIndex = stored.lastIndexOf(":");
@@ -643,22 +617,16 @@ export function createApp(config: JantConfig = {}): App {
643
617
  const now = Math.floor(Date.now() / 1000);
644
618
 
645
619
  if (token !== storedToken || now > expiry) {
646
- return sse(c, async (stream) => {
647
- await stream.toast("Invalid or expired reset link.", "error");
648
- });
620
+ return dsToast("Invalid or expired reset link.", "error");
649
621
  }
650
622
 
651
623
  // Validate passwords
652
624
  if (!password || password.length < 8) {
653
- return sse(c, async (stream) => {
654
- await stream.toast("Password must be at least 8 characters.", "error");
655
- });
625
+ return dsToast("Password must be at least 8 characters.", "error");
656
626
  }
657
627
 
658
628
  if (password !== confirmPassword) {
659
- return sse(c, async (stream) => {
660
- await stream.toast("Passwords do not match.", "error");
661
- });
629
+ return dsToast("Passwords do not match.", "error");
662
630
  }
663
631
 
664
632
  try {
@@ -670,9 +638,7 @@ export function createApp(config: JantConfig = {}): App {
670
638
  .prepare("SELECT id FROM user LIMIT 1")
671
639
  .first<{ id: string }>();
672
640
  if (!userResult) {
673
- return sse(c, async (stream) => {
674
- await stream.toast("No user account found.", "error");
675
- });
641
+ return dsToast("No user account found.", "error");
676
642
  }
677
643
 
678
644
  // Update password
@@ -692,15 +658,11 @@ export function createApp(config: JantConfig = {}): App {
692
658
  // Delete the reset token
693
659
  await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
694
660
 
695
- return sse(c, async (stream) => {
696
- await stream.redirect("/signin?reset");
697
- });
661
+ return dsRedirect("/signin?reset");
698
662
  } catch (err) {
699
663
  // eslint-disable-next-line no-console -- Error logging is intentional
700
664
  console.error("Password reset error:", err);
701
- return sse(c, async (stream) => {
702
- await stream.toast("Failed to reset password.", "error");
703
- });
665
+ return dsToast("Failed to reset password.", "error");
704
666
  }
705
667
  });
706
668
 
package/src/client.ts CHANGED
@@ -10,3 +10,4 @@
10
10
  import "./vendor/datastar.js";
11
11
  import "basecoat-css/all";
12
12
  import "./lib/image-processor.js";
13
+ import "./lib/media-upload.js";