@jant/core 0.2.18 → 0.2.20
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
|
@@ -217,10 +217,3 @@ async function processToFile(
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
export const ImageProcessor = { process, processToFile };
|
|
220
|
-
|
|
221
|
-
// Expose globally for inline scripts
|
|
222
|
-
if (typeof window !== "undefined") {
|
|
223
|
-
(
|
|
224
|
-
window as unknown as { ImageProcessor: typeof ImageProcessor }
|
|
225
|
-
).ImageProcessor = ImageProcessor;
|
|
226
|
-
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side Media Upload Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles file upload flow:
|
|
5
|
+
* 1. User selects file via [data-media-upload] input
|
|
6
|
+
* 2. Creates placeholder in grid with spinner
|
|
7
|
+
* 3. Processes image via ImageProcessor (resize/convert to WebP)
|
|
8
|
+
* 4. Sets processed file on hidden Datastar form via DataTransfer API
|
|
9
|
+
* 5. Triggers form.requestSubmit() — Datastar handles upload + SSE response
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { ImageProcessor } from "./image-processor.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Format file size for display
|
|
16
|
+
*/
|
|
17
|
+
function formatFileSize(bytes: number): string {
|
|
18
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
19
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
20
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensure the media grid exists, removing empty state if needed
|
|
25
|
+
*/
|
|
26
|
+
function ensureGridExists(): HTMLElement {
|
|
27
|
+
let grid = document.getElementById("media-grid");
|
|
28
|
+
if (grid) return grid;
|
|
29
|
+
|
|
30
|
+
document.getElementById("empty-state")?.remove();
|
|
31
|
+
|
|
32
|
+
grid = document.createElement("div");
|
|
33
|
+
grid.id = "media-grid";
|
|
34
|
+
grid.className =
|
|
35
|
+
"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4";
|
|
36
|
+
document.getElementById("media-content")?.appendChild(grid);
|
|
37
|
+
return grid;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a placeholder card with spinner in the media grid
|
|
42
|
+
*/
|
|
43
|
+
function createPlaceholder(
|
|
44
|
+
fileName: string,
|
|
45
|
+
fileSize: number,
|
|
46
|
+
statusText: string,
|
|
47
|
+
): HTMLElement {
|
|
48
|
+
const placeholder = document.createElement("div");
|
|
49
|
+
placeholder.id = "upload-placeholder";
|
|
50
|
+
placeholder.className = "group relative";
|
|
51
|
+
placeholder.innerHTML = `
|
|
52
|
+
<div class="aspect-square bg-muted rounded-lg overflow-hidden border flex items-center justify-center">
|
|
53
|
+
<div class="text-center px-2">
|
|
54
|
+
<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">
|
|
55
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
56
|
+
<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>
|
|
57
|
+
</svg>
|
|
58
|
+
<span id="upload-status" class="text-xs text-muted-foreground">${statusText}</span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="mt-2 text-xs truncate" title="${fileName}">${fileName}</div>
|
|
62
|
+
<div class="text-xs text-muted-foreground">${formatFileSize(fileSize)}</div>
|
|
63
|
+
`;
|
|
64
|
+
return placeholder;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Replace placeholder content with an error message
|
|
69
|
+
*/
|
|
70
|
+
function showPlaceholderError(
|
|
71
|
+
placeholder: HTMLElement,
|
|
72
|
+
fileName: string,
|
|
73
|
+
errorMessage: string,
|
|
74
|
+
): void {
|
|
75
|
+
placeholder.innerHTML = `
|
|
76
|
+
<div class="aspect-square bg-destructive/10 rounded-lg overflow-hidden border border-destructive flex items-center justify-center">
|
|
77
|
+
<div class="text-center px-2">
|
|
78
|
+
<span class="text-xs text-destructive">${errorMessage}</span>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="mt-2 text-xs truncate text-destructive">${fileName}</div>
|
|
82
|
+
<button type="button" class="text-xs text-muted-foreground hover:underline" onclick="this.closest('.group').remove()">Remove</button>
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handle the upload flow for a selected file
|
|
88
|
+
*/
|
|
89
|
+
async function handleUpload(
|
|
90
|
+
input: HTMLInputElement,
|
|
91
|
+
file: File,
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
const processingText = input.dataset.textProcessing || "Processing...";
|
|
94
|
+
const uploadingText = input.dataset.textUploading || "Uploading...";
|
|
95
|
+
const errorText =
|
|
96
|
+
input.dataset.textError || "Upload failed. Please try again.";
|
|
97
|
+
|
|
98
|
+
const grid = ensureGridExists();
|
|
99
|
+
const placeholder = createPlaceholder(file.name, file.size, processingText);
|
|
100
|
+
grid.prepend(placeholder);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Process image client-side (resize, convert to WebP)
|
|
104
|
+
const processed = await ImageProcessor.processToFile(file);
|
|
105
|
+
|
|
106
|
+
// Update status
|
|
107
|
+
const statusEl = document.getElementById("upload-status");
|
|
108
|
+
if (statusEl) statusEl.textContent = uploadingText;
|
|
109
|
+
|
|
110
|
+
// Set processed file on hidden form input via DataTransfer API
|
|
111
|
+
const formInput = document.getElementById(
|
|
112
|
+
"upload-file-input",
|
|
113
|
+
) as HTMLInputElement | null;
|
|
114
|
+
const form = document.getElementById(
|
|
115
|
+
"upload-form",
|
|
116
|
+
) as HTMLFormElement | null;
|
|
117
|
+
if (!formInput || !form) throw new Error("Upload form not found");
|
|
118
|
+
|
|
119
|
+
const dt = new DataTransfer();
|
|
120
|
+
dt.items.add(processed);
|
|
121
|
+
formInput.files = dt.files;
|
|
122
|
+
|
|
123
|
+
// Trigger Datastar-intercepted form submission
|
|
124
|
+
form.requestSubmit();
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const message = err instanceof Error ? err.message : errorText;
|
|
127
|
+
showPlaceholderError(placeholder, file.name, message);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Reset file input so the same file can be re-selected
|
|
131
|
+
input.value = "";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Initialize media upload via event delegation
|
|
136
|
+
*/
|
|
137
|
+
function initMediaUpload(): void {
|
|
138
|
+
document.addEventListener("change", (e) => {
|
|
139
|
+
const input = (e.target as HTMLElement).closest(
|
|
140
|
+
"[data-media-upload]",
|
|
141
|
+
) as HTMLInputElement | null;
|
|
142
|
+
if (!input?.files?.[0]) return;
|
|
143
|
+
|
|
144
|
+
handleUpload(input, input.files[0]);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
initMediaUpload();
|
package/src/lib/sse.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Datastar response utilities for v1.0.0-RC.7
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Provides both SSE (multi-event) and plain HTTP (single-event) response helpers.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* **Non-SSE helpers** (preferred for single operations):
|
|
7
|
+
* - `dsRedirect(url)` — redirect via text/html
|
|
8
|
+
* - `dsToast(message, type)` — toast notification via text/html
|
|
9
|
+
* - `dsSignals(signals)` — signal patch via application/json
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* });
|
|
17
|
-
* ```
|
|
11
|
+
* **SSE** (for multiple operations in one response):
|
|
12
|
+
* - `sse(c, handler)` — streaming SSE with full stream API
|
|
13
|
+
*
|
|
14
|
+
* Datastar auto-detects response type by Content-Type:
|
|
15
|
+
* - `text/html` → dispatches as `datastar-patch-elements`
|
|
16
|
+
* - `application/json` → dispatches as `datastar-patch-signals`
|
|
17
|
+
*
|
|
18
|
+
* @see https://data-star.dev/
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import type { Context } from "hono";
|
|
@@ -124,6 +125,35 @@ export interface SSEStream {
|
|
|
124
125
|
toast(message: string, type?: "success" | "error"): void;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Shared internal helpers (used by both SSE and non-SSE response builders)
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/** Build the redirect script tag for Datastar patch-elements */
|
|
133
|
+
function buildRedirectScript(url: string): string {
|
|
134
|
+
const escapedUrl = url.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
135
|
+
return `<script data-effect="el.remove()">window.location.href='${escapedUrl}'</script>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Build a toast notification HTML element */
|
|
139
|
+
function buildToastHtml(message: string, type: "success" | "error"): string {
|
|
140
|
+
const cls = type === "error" ? "toast-error" : "toast-success";
|
|
141
|
+
const icon =
|
|
142
|
+
type === "error"
|
|
143
|
+
? '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6M9 9l6 6"/></svg>'
|
|
144
|
+
: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>';
|
|
145
|
+
const closeBtn = `<button class="toast-close" data-on:click="el.closest('.toast').classList.add('toast-out'); el.closest('.toast').addEventListener('animationend', () => el.closest('.toast').remove())"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path d="M18 6 6 18M6 6l12 12"/></svg></button>`;
|
|
146
|
+
const escapedMessage = message
|
|
147
|
+
.replace(/&/g, "&")
|
|
148
|
+
.replace(/</g, "<")
|
|
149
|
+
.replace(/>/g, ">");
|
|
150
|
+
return `<div class="toast ${cls}" data-init="setTimeout(() => { el.classList.add('toast-out'); el.addEventListener('animationend', () => el.remove()) }, 3000)">${icon}<span>${escapedMessage}</span>${closeBtn}</div>`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// SSE helpers
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
127
157
|
/**
|
|
128
158
|
* Format a single SSE event string
|
|
129
159
|
*
|
|
@@ -209,10 +239,8 @@ export function sse(
|
|
|
209
239
|
},
|
|
210
240
|
|
|
211
241
|
redirect(url) {
|
|
212
|
-
const escapedUrl = url.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
213
|
-
const script = `<script data-effect="el.remove()">window.location.href='${escapedUrl}'</script>`;
|
|
214
242
|
const dataLines: string[] = [
|
|
215
|
-
`elements ${
|
|
243
|
+
`elements ${buildRedirectScript(url)}`,
|
|
216
244
|
"mode append",
|
|
217
245
|
"selector body",
|
|
218
246
|
];
|
|
@@ -234,19 +262,8 @@ export function sse(
|
|
|
234
262
|
},
|
|
235
263
|
|
|
236
264
|
toast(message, type = "success") {
|
|
237
|
-
const cls = type === "error" ? "toast-error" : "toast-success";
|
|
238
|
-
const icon =
|
|
239
|
-
type === "error"
|
|
240
|
-
? '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6M9 9l6 6"/></svg>'
|
|
241
|
-
: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>';
|
|
242
|
-
const closeBtn = `<button class="toast-close" data-on:click="el.closest('.toast').classList.add('toast-out'); el.closest('.toast').addEventListener('animationend', () => el.closest('.toast').remove())"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path d="M18 6 6 18M6 6l12 12"/></svg></button>`;
|
|
243
|
-
const escapedMessage = message
|
|
244
|
-
.replace(/&/g, "&")
|
|
245
|
-
.replace(/</g, "<")
|
|
246
|
-
.replace(/>/g, ">");
|
|
247
|
-
const html = `<div class="toast ${cls}" data-init="setTimeout(() => { el.classList.add('toast-out'); el.addEventListener('animationend', () => el.remove()) }, 3000)">${icon}<span>${escapedMessage}</span>${closeBtn}</div>`;
|
|
248
265
|
const dataLines: string[] = [
|
|
249
|
-
`elements ${
|
|
266
|
+
`elements ${buildToastHtml(message, type)}`,
|
|
250
267
|
"mode append",
|
|
251
268
|
"selector #toast-container",
|
|
252
269
|
];
|
|
@@ -270,3 +287,88 @@ export function sse(
|
|
|
270
287
|
|
|
271
288
|
return new Response(body, { headers });
|
|
272
289
|
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Non-SSE Datastar helpers (for single-operation responses)
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Datastar redirect via text/html
|
|
297
|
+
*
|
|
298
|
+
* Returns a plain HTML response that Datastar dispatches as `datastar-patch-elements`.
|
|
299
|
+
* Use instead of `sse()` when the only action is a redirect.
|
|
300
|
+
*
|
|
301
|
+
* @param url - The URL to redirect to
|
|
302
|
+
* @param options - Optional extra headers (e.g. Set-Cookie for auth)
|
|
303
|
+
* @returns Response with text/html content-type
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```ts
|
|
307
|
+
* return dsRedirect("/dash/posts");
|
|
308
|
+
*
|
|
309
|
+
* // With cookie forwarding (for auth)
|
|
310
|
+
* return dsRedirect("/dash", { headers: { "Set-Cookie": cookie } });
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
export function dsRedirect(
|
|
314
|
+
url: string,
|
|
315
|
+
options?: { headers?: Record<string, string> },
|
|
316
|
+
): Response {
|
|
317
|
+
return new Response(buildRedirectScript(url), {
|
|
318
|
+
headers: {
|
|
319
|
+
"Content-Type": "text/html",
|
|
320
|
+
"Datastar-Mode": "append",
|
|
321
|
+
"Datastar-Selector": "body",
|
|
322
|
+
...options?.headers,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Datastar toast notification via text/html
|
|
329
|
+
*
|
|
330
|
+
* Returns a plain HTML response that Datastar dispatches as `datastar-patch-elements`.
|
|
331
|
+
* Use instead of `sse()` when the only action is showing a toast.
|
|
332
|
+
*
|
|
333
|
+
* @param message - The message to display
|
|
334
|
+
* @param type - Toast type: "success" (default) or "error"
|
|
335
|
+
* @returns Response with text/html content-type
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```ts
|
|
339
|
+
* return dsToast("Settings saved successfully.");
|
|
340
|
+
* return dsToast("Something went wrong.", "error");
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
export function dsToast(
|
|
344
|
+
message: string,
|
|
345
|
+
type: "success" | "error" = "success",
|
|
346
|
+
): Response {
|
|
347
|
+
return new Response(buildToastHtml(message, type), {
|
|
348
|
+
headers: {
|
|
349
|
+
"Content-Type": "text/html",
|
|
350
|
+
"Datastar-Mode": "append",
|
|
351
|
+
"Datastar-Selector": "#toast-container",
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Datastar signal patch via application/json
|
|
358
|
+
*
|
|
359
|
+
* Returns a JSON response that Datastar dispatches as `datastar-patch-signals`.
|
|
360
|
+
* Use instead of `sse()` when the only action is updating signals.
|
|
361
|
+
*
|
|
362
|
+
* @param signals - Object containing signal values to update
|
|
363
|
+
* @returns Response with application/json content-type
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```ts
|
|
367
|
+
* return dsSignals({ _uploadError: "File too large" });
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
export function dsSignals(signals: Record<string, unknown>): Response {
|
|
371
|
+
return new Response(JSON.stringify(signals), {
|
|
372
|
+
headers: { "Content-Type": "application/json" },
|
|
373
|
+
});
|
|
374
|
+
}
|
package/src/routes/api/upload.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { Bindings } from "../../types.js";
|
|
|
11
11
|
import type { AppVariables } from "../../app.js";
|
|
12
12
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
13
13
|
import { getMediaUrl, getImageUrl } from "../../lib/image.js";
|
|
14
|
-
import { sse } from "../../lib/sse.js";
|
|
14
|
+
import { sse, dsSignals } from "../../lib/sse.js";
|
|
15
15
|
|
|
16
16
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
17
17
|
|
|
@@ -117,11 +117,7 @@ function wantsSSE(c: {
|
|
|
117
117
|
uploadApiRoutes.post("/", async (c) => {
|
|
118
118
|
if (!c.env.R2) {
|
|
119
119
|
if (wantsSSE(c)) {
|
|
120
|
-
return
|
|
121
|
-
await stream.patchSignals({
|
|
122
|
-
_uploadError: "R2 storage not configured",
|
|
123
|
-
});
|
|
124
|
-
});
|
|
120
|
+
return dsSignals({ _uploadError: "R2 storage not configured" });
|
|
125
121
|
}
|
|
126
122
|
return c.json({ error: "R2 storage not configured" }, 500);
|
|
127
123
|
}
|
|
@@ -131,9 +127,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
131
127
|
|
|
132
128
|
if (!file) {
|
|
133
129
|
if (wantsSSE(c)) {
|
|
134
|
-
return
|
|
135
|
-
await stream.patchSignals({ _uploadError: "No file provided" });
|
|
136
|
-
});
|
|
130
|
+
return dsSignals({ _uploadError: "No file provided" });
|
|
137
131
|
}
|
|
138
132
|
return c.json({ error: "No file provided" }, 400);
|
|
139
133
|
}
|
|
@@ -148,9 +142,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
148
142
|
];
|
|
149
143
|
if (!allowedTypes.includes(file.type)) {
|
|
150
144
|
if (wantsSSE(c)) {
|
|
151
|
-
return
|
|
152
|
-
await stream.patchSignals({ _uploadError: "File type not allowed" });
|
|
153
|
-
});
|
|
145
|
+
return dsSignals({ _uploadError: "File type not allowed" });
|
|
154
146
|
}
|
|
155
147
|
return c.json({ error: "File type not allowed" }, 400);
|
|
156
148
|
}
|
|
@@ -159,11 +151,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
159
151
|
const maxSize = 10 * 1024 * 1024;
|
|
160
152
|
if (file.size > maxSize) {
|
|
161
153
|
if (wantsSSE(c)) {
|
|
162
|
-
return
|
|
163
|
-
await stream.patchSignals({
|
|
164
|
-
_uploadError: "File too large (max 10MB)",
|
|
165
|
-
});
|
|
166
|
-
});
|
|
154
|
+
return dsSignals({ _uploadError: "File too large (max 10MB)" });
|
|
167
155
|
}
|
|
168
156
|
return c.json({ error: "File too large (max 10MB)" }, 400);
|
|
169
157
|
}
|
|
@@ -206,6 +194,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
206
194
|
mode: "outer",
|
|
207
195
|
selector: "#upload-placeholder",
|
|
208
196
|
});
|
|
197
|
+
await stream.toast("Upload successful!");
|
|
209
198
|
});
|
|
210
199
|
}
|
|
211
200
|
|
|
@@ -222,7 +211,12 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
222
211
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
223
212
|
console.error("Upload error:", err);
|
|
224
213
|
|
|
225
|
-
|
|
214
|
+
if (wantsSSE(c)) {
|
|
215
|
+
return sse(c, async (stream) => {
|
|
216
|
+
await stream.remove("#upload-placeholder");
|
|
217
|
+
await stream.toast("Upload failed. Please try again.", "error");
|
|
218
|
+
});
|
|
219
|
+
}
|
|
226
220
|
return c.json({ error: "Upload failed" }, 500);
|
|
227
221
|
}
|
|
228
222
|
});
|
|
@@ -7,7 +7,7 @@ import { useLingui } from "@lingui/react/macro";
|
|
|
7
7
|
import type { Bindings } from "../../types.js";
|
|
8
8
|
import type { AppVariables } from "../../app.js";
|
|
9
9
|
import { DashLayout } from "../../theme/layouts/index.js";
|
|
10
|
-
import {
|
|
10
|
+
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
11
11
|
import { getSiteName } from "../../lib/config.js";
|
|
12
12
|
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
13
13
|
import { getAvailableThemes } from "../../lib/theme.js";
|
|
@@ -162,9 +162,7 @@ appearanceRoutes.post("/", async (c) => {
|
|
|
162
162
|
// Validate theme ID
|
|
163
163
|
const validTheme = themes.find((t) => t.id === body.theme);
|
|
164
164
|
if (!validTheme) {
|
|
165
|
-
return
|
|
166
|
-
await stream.toast("Invalid theme selected.", "error");
|
|
167
|
-
});
|
|
165
|
+
return dsToast("Invalid theme selected.", "error");
|
|
168
166
|
}
|
|
169
167
|
|
|
170
168
|
if (validTheme.id === "default") {
|
|
@@ -174,7 +172,5 @@ appearanceRoutes.post("/", async (c) => {
|
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
// Full page reload to apply the new theme CSS
|
|
177
|
-
return
|
|
178
|
-
await stream.redirect("/dash/appearance?saved");
|
|
179
|
-
});
|
|
175
|
+
return dsRedirect("/dash/appearance?saved");
|
|
180
176
|
});
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
DangerZone,
|
|
17
17
|
} from "../../theme/components/index.js";
|
|
18
18
|
import * as sqid from "../../lib/sqid.js";
|
|
19
|
-
import {
|
|
19
|
+
import { dsRedirect } from "../../lib/sse.js";
|
|
20
20
|
|
|
21
21
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
22
22
|
|
|
@@ -409,9 +409,7 @@ collectionsRoutes.post("/", async (c) => {
|
|
|
409
409
|
description: body.description || undefined,
|
|
410
410
|
});
|
|
411
411
|
|
|
412
|
-
return
|
|
413
|
-
await stream.redirect(`/dash/collections/${collection.id}`);
|
|
414
|
-
});
|
|
412
|
+
return dsRedirect(`/dash/collections/${collection.id}`);
|
|
415
413
|
});
|
|
416
414
|
|
|
417
415
|
// View single collection
|
|
@@ -476,9 +474,7 @@ collectionsRoutes.post("/:id", async (c) => {
|
|
|
476
474
|
description: body.description || undefined,
|
|
477
475
|
});
|
|
478
476
|
|
|
479
|
-
return
|
|
480
|
-
await stream.redirect(`/dash/collections/${id}`);
|
|
481
|
-
});
|
|
477
|
+
return dsRedirect(`/dash/collections/${id}`);
|
|
482
478
|
});
|
|
483
479
|
|
|
484
480
|
// Delete collection
|
|
@@ -488,9 +484,7 @@ collectionsRoutes.post("/:id/delete", async (c) => {
|
|
|
488
484
|
|
|
489
485
|
await c.var.services.collections.delete(id);
|
|
490
486
|
|
|
491
|
-
return
|
|
492
|
-
await stream.redirect("/dash/collections");
|
|
493
|
-
});
|
|
487
|
+
return dsRedirect("/dash/collections");
|
|
494
488
|
});
|
|
495
489
|
|
|
496
490
|
// Remove post from collection
|
|
@@ -504,7 +498,5 @@ collectionsRoutes.post("/:id/remove-post", async (c) => {
|
|
|
504
498
|
await c.var.services.collections.removePost(id, body.postId);
|
|
505
499
|
}
|
|
506
500
|
|
|
507
|
-
return
|
|
508
|
-
await stream.redirect(`/dash/collections/${id}`);
|
|
509
|
-
});
|
|
501
|
+
return dsRedirect(`/dash/collections/${id}`);
|
|
510
502
|
});
|