@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.
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +18 -52
- package/dist/client.js +1 -0
- package/dist/lib/image-processor.js +0 -4
- package/dist/lib/media-upload.js +104 -0
- package/dist/lib/sse.d.ts +67 -13
- package/dist/lib/sse.d.ts.map +1 -1
- package/dist/lib/sse.js +108 -23
- package/dist/routes/api/upload.js +16 -18
- package/dist/routes/dash/appearance.js +3 -7
- package/dist/routes/dash/collections.js +5 -13
- package/dist/routes/dash/media.js +17 -167
- package/dist/routes/dash/pages.js +4 -10
- package/dist/routes/dash/posts.js +4 -10
- package/dist/routes/dash/redirects.js +3 -7
- package/dist/routes/dash/settings.d.ts.map +1 -1
- package/dist/routes/dash/settings.js +16 -7
- package/dist/theme/layouts/DashLayout.js +1 -0
- package/package.json +1 -1
- package/src/app.tsx +18 -56
- package/src/client.ts +1 -0
- package/src/lib/image-processor.ts +0 -7
- package/src/lib/media-upload.ts +148 -0
- package/src/lib/sse.ts +130 -28
- package/src/routes/api/upload.ts +12 -18
- package/src/routes/dash/appearance.tsx +3 -7
- package/src/routes/dash/collections.tsx +5 -13
- package/src/routes/dash/media.tsx +16 -165
- package/src/routes/dash/pages.tsx +4 -10
- package/src/routes/dash/posts.tsx +4 -10
- package/src/routes/dash/redirects.tsx +3 -7
- package/src/routes/dash/settings.tsx +25 -7
- package/src/theme/layouts/DashLayout.tsx +1 -1
|
@@ -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 {
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
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("
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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;
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
|
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
|
|
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.");
|
package/package.json
CHANGED
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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