@jant/core 0.2.17 → 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 +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +307 -137
- package/dist/client.js +1 -0
- package/dist/i18n/context.d.ts +2 -2
- package/dist/i18n/context.js +1 -1
- package/dist/i18n/i18n.d.ts +1 -1
- package/dist/i18n/i18n.js +1 -1
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.d.ts.map +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.d.ts.map +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/lib/config.d.ts +44 -10
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +69 -44
- package/dist/lib/constants.d.ts +2 -1
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +5 -2
- package/dist/lib/image-processor.js +0 -4
- package/dist/lib/media-upload.js +104 -0
- package/dist/lib/sse.d.ts +82 -13
- package/dist/lib/sse.d.ts.map +1 -1
- package/dist/lib/sse.js +115 -17
- package/dist/lib/theme.d.ts +44 -0
- package/dist/lib/theme.d.ts.map +1 -0
- package/dist/lib/theme.js +65 -0
- package/dist/routes/api/upload.js +16 -18
- package/dist/routes/dash/appearance.d.ts +13 -0
- package/dist/routes/dash/appearance.d.ts.map +1 -0
- package/dist/routes/dash/appearance.js +160 -0
- 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 +52 -42
- package/dist/services/settings.d.ts +1 -0
- package/dist/services/settings.d.ts.map +1 -1
- package/dist/services/settings.js +3 -0
- package/dist/theme/color-themes.d.ts +30 -0
- package/dist/theme/color-themes.d.ts.map +1 -0
- package/dist/theme/color-themes.js +268 -0
- package/dist/theme/layouts/BaseLayout.d.ts +5 -0
- package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.js +70 -3
- package/dist/theme/layouts/DashLayout.d.ts +2 -0
- package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
- package/dist/theme/layouts/DashLayout.js +11 -1
- package/dist/theme/layouts/index.d.ts +1 -1
- package/dist/theme/layouts/index.d.ts.map +1 -1
- package/dist/types.d.ts +53 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +52 -0
- package/package.json +1 -1
- package/src/app.tsx +260 -81
- package/src/client.ts +1 -0
- package/src/db/migrations/{0000_solid_moon_knight.sql → 0000_square_wallflower.sql} +3 -3
- package/src/db/migrations/meta/0000_snapshot.json +9 -9
- package/src/db/migrations/meta/_journal.json +2 -30
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +1 -1
- package/src/i18n/locales/en.po +328 -252
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +315 -278
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +315 -278
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/config.ts +73 -47
- package/src/lib/constants.ts +3 -0
- package/src/lib/image-processor.ts +0 -7
- package/src/lib/media-upload.ts +148 -0
- package/src/lib/sse.ts +156 -16
- package/src/lib/theme.ts +86 -0
- package/src/preset.css +9 -0
- package/src/routes/api/upload.ts +12 -18
- package/src/routes/dash/appearance.tsx +176 -0
- 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 +71 -55
- package/src/services/settings.ts +5 -0
- package/src/styles/components.css +93 -0
- package/src/theme/color-themes.ts +321 -0
- package/src/theme/layouts/BaseLayout.tsx +61 -1
- package/src/theme/layouts/DashLayout.tsx +14 -3
- package/src/theme/layouts/index.ts +5 -1
- package/src/types.ts +62 -1
- package/src/db/migrations/0001_add_search_fts.sql +0 -40
- package/src/db/migrations/0002_collection_path.sql +0 -2
- package/src/db/migrations/0003_collection_path_nullable.sql +0 -21
- package/src/db/migrations/0004_media_uuid.sql +0 -35
|
@@ -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,10 +4,13 @@ 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";
|
|
8
|
-
import {
|
|
7
|
+
import { sse, dsToast } from "../../lib/sse.js";
|
|
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
|
-
function SettingsContent({ siteName, siteDescription, siteLanguage,
|
|
13
|
+
function SettingsContent({ siteName, siteDescription, siteLanguage, siteNameFallback, siteDescriptionFallback }) {
|
|
11
14
|
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
12
15
|
const generalSignals = JSON.stringify({
|
|
13
16
|
siteName,
|
|
@@ -23,17 +26,6 @@ function SettingsContent({ siteName, siteDescription, siteLanguage, saved }) {
|
|
|
23
26
|
message: "Settings"
|
|
24
27
|
})
|
|
25
28
|
}),
|
|
26
|
-
saved && /*#__PURE__*/ _jsx("div", {
|
|
27
|
-
id: "settings-saved-toast",
|
|
28
|
-
class: "alert mb-4 max-w-lg transition-opacity duration-300",
|
|
29
|
-
"data-init": `console.log('[toast] init fired at', Date.now()); history.replaceState({}, '', '/dash/settings'); setTimeout(() => { console.log('[toast] hiding at', Date.now()); const el = document.getElementById('settings-saved-toast'); if (el) { el.style.opacity = '0'; setTimeout(() => el.remove(), 300) } }, 3000)`,
|
|
30
|
-
children: /*#__PURE__*/ _jsx("h2", {
|
|
31
|
-
children: $__i18n._({
|
|
32
|
-
id: "T0bsor",
|
|
33
|
-
message: "Settings saved successfully."
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
}),
|
|
37
29
|
/*#__PURE__*/ _jsxs("div", {
|
|
38
30
|
class: "flex flex-col gap-6 max-w-lg",
|
|
39
31
|
children: [
|
|
@@ -41,9 +33,6 @@ function SettingsContent({ siteName, siteDescription, siteLanguage, saved }) {
|
|
|
41
33
|
"data-signals": generalSignals,
|
|
42
34
|
"data-on:submit__prevent": "@post('/dash/settings')",
|
|
43
35
|
children: [
|
|
44
|
-
/*#__PURE__*/ _jsx("div", {
|
|
45
|
-
id: "settings-message"
|
|
46
|
-
}),
|
|
47
36
|
/*#__PURE__*/ _jsxs("div", {
|
|
48
37
|
class: "card",
|
|
49
38
|
children: [
|
|
@@ -72,7 +61,7 @@ function SettingsContent({ siteName, siteDescription, siteLanguage, saved }) {
|
|
|
72
61
|
type: "text",
|
|
73
62
|
"data-bind": "siteName",
|
|
74
63
|
class: "input",
|
|
75
|
-
|
|
64
|
+
placeholder: siteNameFallback
|
|
76
65
|
})
|
|
77
66
|
]
|
|
78
67
|
}),
|
|
@@ -90,6 +79,7 @@ function SettingsContent({ siteName, siteDescription, siteLanguage, saved }) {
|
|
|
90
79
|
"data-bind": "siteDescription",
|
|
91
80
|
class: "textarea",
|
|
92
81
|
rows: 3,
|
|
82
|
+
placeholder: siteDescriptionFallback,
|
|
93
83
|
children: siteDescription
|
|
94
84
|
})
|
|
95
85
|
]
|
|
@@ -145,9 +135,6 @@ function SettingsContent({ siteName, siteDescription, siteLanguage, saved }) {
|
|
|
145
135
|
"data-signals": "{currentPassword: '', newPassword: '', confirmPassword: ''}",
|
|
146
136
|
"data-on:submit__prevent": "@post('/dash/settings/password')",
|
|
147
137
|
children: [
|
|
148
|
-
/*#__PURE__*/ _jsx("div", {
|
|
149
|
-
id: "password-message"
|
|
150
|
-
}),
|
|
151
138
|
/*#__PURE__*/ _jsxs("div", {
|
|
152
139
|
class: "card",
|
|
153
140
|
children: [
|
|
@@ -242,40 +229,67 @@ function SettingsContent({ siteName, siteDescription, siteLanguage, saved }) {
|
|
|
242
229
|
}
|
|
243
230
|
// Settings page
|
|
244
231
|
settingsRoutes.get("/", async (c)=>{
|
|
245
|
-
const
|
|
246
|
-
|
|
232
|
+
const { settings } = c.var.services;
|
|
233
|
+
// Fetch raw DB values (null if not set)
|
|
234
|
+
const dbSiteName = await settings.get("SITE_NAME");
|
|
235
|
+
const dbSiteDescription = await settings.get("SITE_DESCRIPTION");
|
|
247
236
|
const siteLanguage = await getSiteLanguage(c);
|
|
237
|
+
// Fallback values (ENV > Default) for placeholders
|
|
238
|
+
const siteNameFallback = getConfigFallback(c, "SITE_NAME");
|
|
239
|
+
const siteDescriptionFallback = getConfigFallback(c, "SITE_DESCRIPTION");
|
|
248
240
|
const saved = c.req.query("saved") !== undefined;
|
|
249
241
|
return c.html(/*#__PURE__*/ _jsx(DashLayout, {
|
|
250
242
|
c: c,
|
|
251
243
|
title: "Settings",
|
|
252
|
-
siteName:
|
|
244
|
+
siteName: dbSiteName || siteNameFallback,
|
|
253
245
|
currentPath: "/dash/settings",
|
|
246
|
+
toast: saved ? {
|
|
247
|
+
message: "Settings saved successfully."
|
|
248
|
+
} : undefined,
|
|
254
249
|
children: /*#__PURE__*/ _jsx(SettingsContent, {
|
|
255
|
-
siteName:
|
|
256
|
-
siteDescription:
|
|
250
|
+
siteName: dbSiteName || "",
|
|
251
|
+
siteDescription: dbSiteDescription || "",
|
|
257
252
|
siteLanguage: siteLanguage,
|
|
258
|
-
|
|
253
|
+
siteNameFallback: siteNameFallback,
|
|
254
|
+
siteDescriptionFallback: siteDescriptionFallback
|
|
259
255
|
})
|
|
260
256
|
}));
|
|
261
257
|
});
|
|
262
258
|
// Update settings
|
|
263
259
|
settingsRoutes.post("/", async (c)=>{
|
|
264
260
|
const body = await c.req.json();
|
|
265
|
-
const
|
|
266
|
-
await
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
261
|
+
const { settings } = c.var.services;
|
|
262
|
+
const oldLanguage = await settings.get("SITE_LANGUAGE") ?? "en";
|
|
263
|
+
// For text fields: empty = remove from DB (fall back to ENV > Default)
|
|
264
|
+
if (body.siteName.trim()) {
|
|
265
|
+
await settings.set("SITE_NAME", body.siteName.trim());
|
|
266
|
+
} else {
|
|
267
|
+
await settings.remove("SITE_NAME");
|
|
268
|
+
}
|
|
269
|
+
if (body.siteDescription.trim()) {
|
|
270
|
+
await settings.set("SITE_DESCRIPTION", body.siteDescription.trim());
|
|
271
|
+
} else {
|
|
272
|
+
await settings.remove("SITE_DESCRIPTION");
|
|
273
|
+
}
|
|
274
|
+
// Language always has a value from the select
|
|
275
|
+
await settings.set("SITE_LANGUAGE", body.siteLanguage);
|
|
271
276
|
const languageChanged = oldLanguage !== body.siteLanguage;
|
|
277
|
+
// Determine the effective display name after save
|
|
278
|
+
const displayName = body.siteName.trim() || getConfigFallback(c, "SITE_NAME");
|
|
272
279
|
return sse(c, async (stream)=>{
|
|
273
280
|
if (languageChanged) {
|
|
274
281
|
// Language changed - full reload needed to update all UI text
|
|
275
282
|
await stream.redirect("/dash/settings?saved");
|
|
276
283
|
} else {
|
|
277
|
-
|
|
278
|
-
|
|
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
|
+
});
|
|
292
|
+
await stream.toast("Settings saved successfully.");
|
|
279
293
|
}
|
|
280
294
|
});
|
|
281
295
|
});
|
|
@@ -283,9 +297,7 @@ settingsRoutes.post("/", async (c)=>{
|
|
|
283
297
|
settingsRoutes.post("/password", async (c)=>{
|
|
284
298
|
const body = await c.req.json();
|
|
285
299
|
if (body.newPassword !== body.confirmPassword) {
|
|
286
|
-
return
|
|
287
|
-
await stream.patchElements('<div id="password-message"><div class="alert-destructive mb-4"><h2>Passwords do not match.</h2></div></div>');
|
|
288
|
-
});
|
|
300
|
+
return dsToast("Passwords do not match.", "error");
|
|
289
301
|
}
|
|
290
302
|
try {
|
|
291
303
|
await c.var.auth.api.changePassword({
|
|
@@ -297,12 +309,10 @@ settingsRoutes.post("/password", async (c)=>{
|
|
|
297
309
|
headers: c.req.raw.headers
|
|
298
310
|
});
|
|
299
311
|
} catch {
|
|
300
|
-
return
|
|
301
|
-
await stream.patchElements('<div id="password-message"><div class="alert-destructive mb-4"><h2>Current password is incorrect.</h2></div></div>');
|
|
302
|
-
});
|
|
312
|
+
return dsToast("Current password is incorrect.", "error");
|
|
303
313
|
}
|
|
304
314
|
return sse(c, async (stream)=>{
|
|
305
|
-
await stream.
|
|
315
|
+
await stream.toast("Password changed successfully.");
|
|
306
316
|
await stream.patchSignals({
|
|
307
317
|
currentPassword: "",
|
|
308
318
|
newPassword: "",
|
|
@@ -10,6 +10,7 @@ export interface SettingsService {
|
|
|
10
10
|
getAll(): Promise<Record<string, string>>;
|
|
11
11
|
set(key: SettingsKey, value: string): Promise<void>;
|
|
12
12
|
setMany(entries: Partial<Record<SettingsKey, string>>): Promise<void>;
|
|
13
|
+
remove(key: SettingsKey): Promise<void>;
|
|
13
14
|
isOnboardingComplete(): Promise<boolean>;
|
|
14
15
|
completeOnboarding(): Promise<void>;
|
|
15
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/services/settings.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG/C,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9C,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1C,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,GAAG,eAAe,
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/services/settings.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG/C,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9C,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1C,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,MAAM,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,GAAG,eAAe,CAiEnE"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Color Themes
|
|
3
|
+
*
|
|
4
|
+
* Each theme defines CSS variable overrides for light and dark modes,
|
|
5
|
+
* plus preview colors for the theme picker UI.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* A color theme definition with light and dark mode CSS variable overrides.
|
|
9
|
+
*/
|
|
10
|
+
export interface ColorTheme {
|
|
11
|
+
/** Stored in DB settings, e.g. "beach" */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Display name, e.g. "Beach" */
|
|
14
|
+
name: string;
|
|
15
|
+
/** CSS variable overrides for :root (light mode) */
|
|
16
|
+
light: Record<string, string>;
|
|
17
|
+
/** CSS variable overrides for .dark (dark mode) */
|
|
18
|
+
dark: Record<string, string>;
|
|
19
|
+
/** Preview colors (hex) for theme picker cards */
|
|
20
|
+
preview: {
|
|
21
|
+
lightBg: string;
|
|
22
|
+
lightText: string;
|
|
23
|
+
lightLink: string;
|
|
24
|
+
darkBg: string;
|
|
25
|
+
darkText: string;
|
|
26
|
+
darkLink: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare const BUILTIN_COLOR_THEMES: ColorTheme[];
|
|
30
|
+
//# sourceMappingURL=color-themes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"color-themes.d.ts","sourceRoot":"","sources":["../../src/theme/color-themes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,kDAAkD;IAClD,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AA2FD,eAAO,MAAM,oBAAoB,EAAE,UAAU,EAyM5C,CAAC"}
|