@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
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwCvD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;AAExE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,MAAM,GAAE,UAAe,GAAG,GAAG,
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGlD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwCvD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC;AAExE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,SAAS,CAAC,MAAM,GAAE,UAAe,GAAG,GAAG,CAyoBtD"}
|
package/dist/app.js
CHANGED
|
@@ -36,7 +36,7 @@ import { sitemapRoutes } from "./routes/feed/sitemap.js";
|
|
|
36
36
|
import { requireAuth } from "./middleware/auth.js";
|
|
37
37
|
// Layouts for auth pages
|
|
38
38
|
import { BaseLayout } from "./theme/layouts/index.js";
|
|
39
|
-
import {
|
|
39
|
+
import { dsRedirect, dsToast } from "./lib/sse.js";
|
|
40
40
|
import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
|
|
41
41
|
/**
|
|
42
42
|
* Create a Jant application
|
|
@@ -247,19 +247,13 @@ import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
|
|
|
247
247
|
const body = await c.req.json();
|
|
248
248
|
const { name, email, password } = body;
|
|
249
249
|
if (!name || !email || !password) {
|
|
250
|
-
return
|
|
251
|
-
await stream.toast("All fields are required", "error");
|
|
252
|
-
});
|
|
250
|
+
return dsToast("All fields are required", "error");
|
|
253
251
|
}
|
|
254
252
|
if (password.length < 8) {
|
|
255
|
-
return
|
|
256
|
-
await stream.toast("Password must be at least 8 characters", "error");
|
|
257
|
-
});
|
|
253
|
+
return dsToast("Password must be at least 8 characters", "error");
|
|
258
254
|
}
|
|
259
255
|
if (!c.var.auth) {
|
|
260
|
-
return
|
|
261
|
-
await stream.toast("AUTH_SECRET not configured", "error");
|
|
262
|
-
});
|
|
256
|
+
return dsToast("AUTH_SECRET not configured", "error");
|
|
263
257
|
}
|
|
264
258
|
try {
|
|
265
259
|
const signUpResponse = await c.var.auth.api.signUpEmail({
|
|
@@ -270,20 +264,14 @@ import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
|
|
|
270
264
|
}
|
|
271
265
|
});
|
|
272
266
|
if (!signUpResponse || "error" in signUpResponse) {
|
|
273
|
-
return
|
|
274
|
-
await stream.toast("Failed to create account", "error");
|
|
275
|
-
});
|
|
267
|
+
return dsToast("Failed to create account", "error");
|
|
276
268
|
}
|
|
277
269
|
await c.var.services.settings.completeOnboarding();
|
|
278
|
-
return
|
|
279
|
-
await stream.redirect("/signin?setup");
|
|
280
|
-
});
|
|
270
|
+
return dsRedirect("/signin?setup");
|
|
281
271
|
} catch (err) {
|
|
282
272
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
283
273
|
console.error("Setup error:", err);
|
|
284
|
-
return
|
|
285
|
-
await stream.toast("Failed to create account", "error");
|
|
286
|
-
});
|
|
274
|
+
return dsToast("Failed to create account", "error");
|
|
287
275
|
}
|
|
288
276
|
});
|
|
289
277
|
// Signin page component
|
|
@@ -398,9 +386,7 @@ import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
|
|
|
398
386
|
});
|
|
399
387
|
app.post("/signin", async (c)=>{
|
|
400
388
|
if (!c.var.auth) {
|
|
401
|
-
return
|
|
402
|
-
await stream.toast("Auth not configured", "error");
|
|
403
|
-
});
|
|
389
|
+
return dsToast("Auth not configured", "error");
|
|
404
390
|
}
|
|
405
391
|
const body = await c.req.json();
|
|
406
392
|
const { email, password } = body;
|
|
@@ -417,9 +403,7 @@ import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
|
|
|
417
403
|
});
|
|
418
404
|
const response = await c.var.auth.handler(signInRequest);
|
|
419
405
|
if (!response.ok) {
|
|
420
|
-
return
|
|
421
|
-
await stream.toast("Invalid email or password", "error");
|
|
422
|
-
});
|
|
406
|
+
return dsToast("Invalid email or password", "error");
|
|
423
407
|
}
|
|
424
408
|
// Forward Set-Cookie headers from auth response
|
|
425
409
|
const cookieHeaders = {};
|
|
@@ -427,17 +411,13 @@ import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
|
|
|
427
411
|
if (setCookie) {
|
|
428
412
|
cookieHeaders["Set-Cookie"] = setCookie;
|
|
429
413
|
}
|
|
430
|
-
return
|
|
431
|
-
await stream.redirect("/dash");
|
|
432
|
-
}, {
|
|
414
|
+
return dsRedirect("/dash", {
|
|
433
415
|
headers: cookieHeaders
|
|
434
416
|
});
|
|
435
417
|
} catch (err) {
|
|
436
418
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
437
419
|
console.error("Signin error:", err);
|
|
438
|
-
return
|
|
439
|
-
await stream.toast("Invalid email or password", "error");
|
|
440
|
-
});
|
|
420
|
+
return dsToast("Invalid email or password", "error");
|
|
441
421
|
}
|
|
442
422
|
});
|
|
443
423
|
app.get("/signout", async (c)=>{
|
|
@@ -612,29 +592,21 @@ import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
|
|
|
612
592
|
// Validate token
|
|
613
593
|
const stored = await c.var.services.settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
614
594
|
if (!stored) {
|
|
615
|
-
return
|
|
616
|
-
await stream.toast("Invalid or expired reset link.", "error");
|
|
617
|
-
});
|
|
595
|
+
return dsToast("Invalid or expired reset link.", "error");
|
|
618
596
|
}
|
|
619
597
|
const separatorIndex = stored.lastIndexOf(":");
|
|
620
598
|
const storedToken = stored.substring(0, separatorIndex);
|
|
621
599
|
const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
|
|
622
600
|
const now = Math.floor(Date.now() / 1000);
|
|
623
601
|
if (token !== storedToken || now > expiry) {
|
|
624
|
-
return
|
|
625
|
-
await stream.toast("Invalid or expired reset link.", "error");
|
|
626
|
-
});
|
|
602
|
+
return dsToast("Invalid or expired reset link.", "error");
|
|
627
603
|
}
|
|
628
604
|
// Validate passwords
|
|
629
605
|
if (!password || password.length < 8) {
|
|
630
|
-
return
|
|
631
|
-
await stream.toast("Password must be at least 8 characters.", "error");
|
|
632
|
-
});
|
|
606
|
+
return dsToast("Password must be at least 8 characters.", "error");
|
|
633
607
|
}
|
|
634
608
|
if (password !== confirmPassword) {
|
|
635
|
-
return
|
|
636
|
-
await stream.toast("Passwords do not match.", "error");
|
|
637
|
-
});
|
|
609
|
+
return dsToast("Passwords do not match.", "error");
|
|
638
610
|
}
|
|
639
611
|
try {
|
|
640
612
|
const hashedPassword = await hashPassword(password);
|
|
@@ -642,9 +614,7 @@ import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
|
|
|
642
614
|
// Get admin user
|
|
643
615
|
const userResult = await db.prepare("SELECT id FROM user LIMIT 1").first();
|
|
644
616
|
if (!userResult) {
|
|
645
|
-
return
|
|
646
|
-
await stream.toast("No user account found.", "error");
|
|
647
|
-
});
|
|
617
|
+
return dsToast("No user account found.", "error");
|
|
648
618
|
}
|
|
649
619
|
// Update password
|
|
650
620
|
await db.prepare("UPDATE account SET password = ? WHERE user_id = ? AND provider_id = 'credential'").bind(hashedPassword, userResult.id).run();
|
|
@@ -652,15 +622,11 @@ import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
|
|
|
652
622
|
await db.prepare("DELETE FROM session WHERE user_id = ?").bind(userResult.id).run();
|
|
653
623
|
// Delete the reset token
|
|
654
624
|
await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
655
|
-
return
|
|
656
|
-
await stream.redirect("/signin?reset");
|
|
657
|
-
});
|
|
625
|
+
return dsRedirect("/signin?reset");
|
|
658
626
|
} catch (err) {
|
|
659
627
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
660
628
|
console.error("Password reset error:", err);
|
|
661
|
-
return
|
|
662
|
-
await stream.toast("Failed to reset password.", "error");
|
|
663
|
-
});
|
|
629
|
+
return dsToast("Failed to reset password.", "error");
|
|
664
630
|
}
|
|
665
631
|
});
|
|
666
632
|
// Dashboard routes (protected)
|
package/dist/client.js
CHANGED
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
*/ import { ImageProcessor } from "./image-processor.js";
|
|
11
|
+
/**
|
|
12
|
+
* Format file size for display
|
|
13
|
+
*/ function formatFileSize(bytes) {
|
|
14
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
15
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
16
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Ensure the media grid exists, removing empty state if needed
|
|
20
|
+
*/ function ensureGridExists() {
|
|
21
|
+
let grid = document.getElementById("media-grid");
|
|
22
|
+
if (grid) return grid;
|
|
23
|
+
document.getElementById("empty-state")?.remove();
|
|
24
|
+
grid = document.createElement("div");
|
|
25
|
+
grid.id = "media-grid";
|
|
26
|
+
grid.className = "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4";
|
|
27
|
+
document.getElementById("media-content")?.appendChild(grid);
|
|
28
|
+
return grid;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a placeholder card with spinner in the media grid
|
|
32
|
+
*/ function createPlaceholder(fileName, fileSize, statusText) {
|
|
33
|
+
const placeholder = document.createElement("div");
|
|
34
|
+
placeholder.id = "upload-placeholder";
|
|
35
|
+
placeholder.className = "group relative";
|
|
36
|
+
placeholder.innerHTML = `
|
|
37
|
+
<div class="aspect-square bg-muted rounded-lg overflow-hidden border flex items-center justify-center">
|
|
38
|
+
<div class="text-center px-2">
|
|
39
|
+
<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">
|
|
40
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
41
|
+
<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>
|
|
42
|
+
</svg>
|
|
43
|
+
<span id="upload-status" class="text-xs text-muted-foreground">${statusText}</span>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="mt-2 text-xs truncate" title="${fileName}">${fileName}</div>
|
|
47
|
+
<div class="text-xs text-muted-foreground">${formatFileSize(fileSize)}</div>
|
|
48
|
+
`;
|
|
49
|
+
return placeholder;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Replace placeholder content with an error message
|
|
53
|
+
*/ function showPlaceholderError(placeholder, fileName, errorMessage) {
|
|
54
|
+
placeholder.innerHTML = `
|
|
55
|
+
<div class="aspect-square bg-destructive/10 rounded-lg overflow-hidden border border-destructive flex items-center justify-center">
|
|
56
|
+
<div class="text-center px-2">
|
|
57
|
+
<span class="text-xs text-destructive">${errorMessage}</span>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="mt-2 text-xs truncate text-destructive">${fileName}</div>
|
|
61
|
+
<button type="button" class="text-xs text-muted-foreground hover:underline" onclick="this.closest('.group').remove()">Remove</button>
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Handle the upload flow for a selected file
|
|
66
|
+
*/ async function handleUpload(input, file) {
|
|
67
|
+
const processingText = input.dataset.textProcessing || "Processing...";
|
|
68
|
+
const uploadingText = input.dataset.textUploading || "Uploading...";
|
|
69
|
+
const errorText = input.dataset.textError || "Upload failed. Please try again.";
|
|
70
|
+
const grid = ensureGridExists();
|
|
71
|
+
const placeholder = createPlaceholder(file.name, file.size, processingText);
|
|
72
|
+
grid.prepend(placeholder);
|
|
73
|
+
try {
|
|
74
|
+
// Process image client-side (resize, convert to WebP)
|
|
75
|
+
const processed = await ImageProcessor.processToFile(file);
|
|
76
|
+
// Update status
|
|
77
|
+
const statusEl = document.getElementById("upload-status");
|
|
78
|
+
if (statusEl) statusEl.textContent = uploadingText;
|
|
79
|
+
// Set processed file on hidden form input via DataTransfer API
|
|
80
|
+
const formInput = document.getElementById("upload-file-input");
|
|
81
|
+
const form = document.getElementById("upload-form");
|
|
82
|
+
if (!formInput || !form) throw new Error("Upload form not found");
|
|
83
|
+
const dt = new DataTransfer();
|
|
84
|
+
dt.items.add(processed);
|
|
85
|
+
formInput.files = dt.files;
|
|
86
|
+
// Trigger Datastar-intercepted form submission
|
|
87
|
+
form.requestSubmit();
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : errorText;
|
|
90
|
+
showPlaceholderError(placeholder, file.name, message);
|
|
91
|
+
}
|
|
92
|
+
// Reset file input so the same file can be re-selected
|
|
93
|
+
input.value = "";
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Initialize media upload via event delegation
|
|
97
|
+
*/ function initMediaUpload() {
|
|
98
|
+
document.addEventListener("change", (e)=>{
|
|
99
|
+
const input = e.target.closest("[data-media-upload]");
|
|
100
|
+
if (!input?.files?.[0]) return;
|
|
101
|
+
handleUpload(input, input.files[0]);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
initMediaUpload();
|
package/dist/lib/sse.d.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
|
import type { Context } from "hono";
|
|
20
21
|
/**
|
|
@@ -135,4 +136,57 @@ export interface SSEStream {
|
|
|
135
136
|
export declare function sse(c: Context, handler: (stream: SSEStream) => Promise<void>, options?: {
|
|
136
137
|
headers?: Record<string, string>;
|
|
137
138
|
}): Response;
|
|
139
|
+
/**
|
|
140
|
+
* Datastar redirect via text/html
|
|
141
|
+
*
|
|
142
|
+
* Returns a plain HTML response that Datastar dispatches as `datastar-patch-elements`.
|
|
143
|
+
* Use instead of `sse()` when the only action is a redirect.
|
|
144
|
+
*
|
|
145
|
+
* @param url - The URL to redirect to
|
|
146
|
+
* @param options - Optional extra headers (e.g. Set-Cookie for auth)
|
|
147
|
+
* @returns Response with text/html content-type
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* return dsRedirect("/dash/posts");
|
|
152
|
+
*
|
|
153
|
+
* // With cookie forwarding (for auth)
|
|
154
|
+
* return dsRedirect("/dash", { headers: { "Set-Cookie": cookie } });
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export declare function dsRedirect(url: string, options?: {
|
|
158
|
+
headers?: Record<string, string>;
|
|
159
|
+
}): Response;
|
|
160
|
+
/**
|
|
161
|
+
* Datastar toast notification via text/html
|
|
162
|
+
*
|
|
163
|
+
* Returns a plain HTML response that Datastar dispatches as `datastar-patch-elements`.
|
|
164
|
+
* Use instead of `sse()` when the only action is showing a toast.
|
|
165
|
+
*
|
|
166
|
+
* @param message - The message to display
|
|
167
|
+
* @param type - Toast type: "success" (default) or "error"
|
|
168
|
+
* @returns Response with text/html content-type
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* return dsToast("Settings saved successfully.");
|
|
173
|
+
* return dsToast("Something went wrong.", "error");
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export declare function dsToast(message: string, type?: "success" | "error"): Response;
|
|
177
|
+
/**
|
|
178
|
+
* Datastar signal patch via application/json
|
|
179
|
+
*
|
|
180
|
+
* Returns a JSON response that Datastar dispatches as `datastar-patch-signals`.
|
|
181
|
+
* Use instead of `sse()` when the only action is updating signals.
|
|
182
|
+
*
|
|
183
|
+
* @param signals - Object containing signal values to update
|
|
184
|
+
* @returns Response with application/json content-type
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* return dsSignals({ _uploadError: "File too large" });
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export declare function dsSignals(signals: Record<string, unknown>): Response;
|
|
138
192
|
//# sourceMappingURL=sse.d.ts.map
|
package/dist/lib/sse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/lib/sse.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/lib/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC;;;;GAIG;AACH,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,OAAO,GACP,SAAS,GACT,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,CAAC;AAEb;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB;;;;;;;;;;OAUG;IACH,YAAY,CACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GACpC,IAAI,CAAC;IAER;;;;;;;;;;;;;;;;;OAiBG;IACH,aAAa,CACX,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,SAAS,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,GACA,IAAI,CAAC;IAER;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B;;;;;;;;;OASG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAE/B;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,IAAI,CAAC;CAC1D;AA+CD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,GAAG,CACjB,CAAC,EAAE,OAAO,EACV,OAAO,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,EAC7C,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAC7C,QAAQ,CAoFV;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAC7C,QAAQ,CASV;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,CACrB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,SAAS,GAAG,OAAmB,GACpC,QAAQ,CAQV;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,QAAQ,CAIpE"}
|
package/dist/lib/sse.js
CHANGED
|
@@ -1,21 +1,39 @@
|
|
|
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
|
-
|
|
18
|
-
|
|
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/
|
|
19
|
+
*/ // ---------------------------------------------------------------------------
|
|
20
|
+
// Shared internal helpers (used by both SSE and non-SSE response builders)
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/** Build the redirect script tag for Datastar patch-elements */ function buildRedirectScript(url) {
|
|
23
|
+
const escapedUrl = url.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
24
|
+
return `<script data-effect="el.remove()">window.location.href='${escapedUrl}'</script>`;
|
|
25
|
+
}
|
|
26
|
+
/** Build a toast notification HTML element */ function buildToastHtml(message, type) {
|
|
27
|
+
const cls = type === "error" ? "toast-error" : "toast-success";
|
|
28
|
+
const icon = type === "error" ? '<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>' : '<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>';
|
|
29
|
+
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>`;
|
|
30
|
+
const escapedMessage = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
31
|
+
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>`;
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// SSE helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/**
|
|
19
37
|
* Format a single SSE event string
|
|
20
38
|
*
|
|
21
39
|
* @param eventType - The Datastar event type (e.g. "datastar-patch-elements")
|
|
@@ -88,10 +106,8 @@
|
|
|
88
106
|
controller.enqueue(encoder.encode(formatEvent("datastar-patch-elements", dataLines)));
|
|
89
107
|
},
|
|
90
108
|
redirect (url) {
|
|
91
|
-
const escapedUrl = url.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
92
|
-
const script = `<script data-effect="el.remove()">window.location.href='${escapedUrl}'</script>`;
|
|
93
109
|
const dataLines = [
|
|
94
|
-
`elements ${
|
|
110
|
+
`elements ${buildRedirectScript(url)}`,
|
|
95
111
|
"mode append",
|
|
96
112
|
"selector body"
|
|
97
113
|
];
|
|
@@ -105,13 +121,8 @@
|
|
|
105
121
|
])));
|
|
106
122
|
},
|
|
107
123
|
toast (message, type = "success") {
|
|
108
|
-
const cls = type === "error" ? "toast-error" : "toast-success";
|
|
109
|
-
const icon = type === "error" ? '<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>' : '<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>';
|
|
110
|
-
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>`;
|
|
111
|
-
const escapedMessage = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
112
|
-
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>`;
|
|
113
124
|
const dataLines = [
|
|
114
|
-
`elements ${
|
|
125
|
+
`elements ${buildToastHtml(message, type)}`,
|
|
115
126
|
"mode append",
|
|
116
127
|
"selector #toast-container"
|
|
117
128
|
];
|
|
@@ -132,3 +143,77 @@
|
|
|
132
143
|
headers
|
|
133
144
|
});
|
|
134
145
|
}
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Non-SSE Datastar helpers (for single-operation responses)
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
/**
|
|
150
|
+
* Datastar redirect via text/html
|
|
151
|
+
*
|
|
152
|
+
* Returns a plain HTML response that Datastar dispatches as `datastar-patch-elements`.
|
|
153
|
+
* Use instead of `sse()` when the only action is a redirect.
|
|
154
|
+
*
|
|
155
|
+
* @param url - The URL to redirect to
|
|
156
|
+
* @param options - Optional extra headers (e.g. Set-Cookie for auth)
|
|
157
|
+
* @returns Response with text/html content-type
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* return dsRedirect("/dash/posts");
|
|
162
|
+
*
|
|
163
|
+
* // With cookie forwarding (for auth)
|
|
164
|
+
* return dsRedirect("/dash", { headers: { "Set-Cookie": cookie } });
|
|
165
|
+
* ```
|
|
166
|
+
*/ export function dsRedirect(url, options) {
|
|
167
|
+
return new Response(buildRedirectScript(url), {
|
|
168
|
+
headers: {
|
|
169
|
+
"Content-Type": "text/html",
|
|
170
|
+
"Datastar-Mode": "append",
|
|
171
|
+
"Datastar-Selector": "body",
|
|
172
|
+
...options?.headers
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Datastar toast notification via text/html
|
|
178
|
+
*
|
|
179
|
+
* Returns a plain HTML response that Datastar dispatches as `datastar-patch-elements`.
|
|
180
|
+
* Use instead of `sse()` when the only action is showing a toast.
|
|
181
|
+
*
|
|
182
|
+
* @param message - The message to display
|
|
183
|
+
* @param type - Toast type: "success" (default) or "error"
|
|
184
|
+
* @returns Response with text/html content-type
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* return dsToast("Settings saved successfully.");
|
|
189
|
+
* return dsToast("Something went wrong.", "error");
|
|
190
|
+
* ```
|
|
191
|
+
*/ export function dsToast(message, type = "success") {
|
|
192
|
+
return new Response(buildToastHtml(message, type), {
|
|
193
|
+
headers: {
|
|
194
|
+
"Content-Type": "text/html",
|
|
195
|
+
"Datastar-Mode": "append",
|
|
196
|
+
"Datastar-Selector": "#toast-container"
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Datastar signal patch via application/json
|
|
202
|
+
*
|
|
203
|
+
* Returns a JSON response that Datastar dispatches as `datastar-patch-signals`.
|
|
204
|
+
* Use instead of `sse()` when the only action is updating signals.
|
|
205
|
+
*
|
|
206
|
+
* @param signals - Object containing signal values to update
|
|
207
|
+
* @returns Response with application/json content-type
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* return dsSignals({ _uploadError: "File too large" });
|
|
212
|
+
* ```
|
|
213
|
+
*/ export function dsSignals(signals) {
|
|
214
|
+
return new Response(JSON.stringify(signals), {
|
|
215
|
+
headers: {
|
|
216
|
+
"Content-Type": "application/json"
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { html } from "hono/html";
|
|
8
8
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
9
9
|
import { getMediaUrl, getImageUrl } from "../../lib/image.js";
|
|
10
|
-
import { sse } from "../../lib/sse.js";
|
|
10
|
+
import { sse, dsSignals } from "../../lib/sse.js";
|
|
11
11
|
export const uploadApiRoutes = new Hono();
|
|
12
12
|
// Require auth for all upload routes
|
|
13
13
|
uploadApiRoutes.use("*", requireAuthApi());
|
|
@@ -88,10 +88,8 @@ function formatSize(bytes) {
|
|
|
88
88
|
uploadApiRoutes.post("/", async (c)=>{
|
|
89
89
|
if (!c.env.R2) {
|
|
90
90
|
if (wantsSSE(c)) {
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
_uploadError: "R2 storage not configured"
|
|
94
|
-
});
|
|
91
|
+
return dsSignals({
|
|
92
|
+
_uploadError: "R2 storage not configured"
|
|
95
93
|
});
|
|
96
94
|
}
|
|
97
95
|
return c.json({
|
|
@@ -102,10 +100,8 @@ uploadApiRoutes.post("/", async (c)=>{
|
|
|
102
100
|
const file = formData.get("file");
|
|
103
101
|
if (!file) {
|
|
104
102
|
if (wantsSSE(c)) {
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
_uploadError: "No file provided"
|
|
108
|
-
});
|
|
103
|
+
return dsSignals({
|
|
104
|
+
_uploadError: "No file provided"
|
|
109
105
|
});
|
|
110
106
|
}
|
|
111
107
|
return c.json({
|
|
@@ -122,10 +118,8 @@ uploadApiRoutes.post("/", async (c)=>{
|
|
|
122
118
|
];
|
|
123
119
|
if (!allowedTypes.includes(file.type)) {
|
|
124
120
|
if (wantsSSE(c)) {
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
_uploadError: "File type not allowed"
|
|
128
|
-
});
|
|
121
|
+
return dsSignals({
|
|
122
|
+
_uploadError: "File type not allowed"
|
|
129
123
|
});
|
|
130
124
|
}
|
|
131
125
|
return c.json({
|
|
@@ -136,10 +130,8 @@ uploadApiRoutes.post("/", async (c)=>{
|
|
|
136
130
|
const maxSize = 10 * 1024 * 1024;
|
|
137
131
|
if (file.size > maxSize) {
|
|
138
132
|
if (wantsSSE(c)) {
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
_uploadError: "File too large (max 10MB)"
|
|
142
|
-
});
|
|
133
|
+
return dsSignals({
|
|
134
|
+
_uploadError: "File too large (max 10MB)"
|
|
143
135
|
});
|
|
144
136
|
}
|
|
145
137
|
return c.json({
|
|
@@ -176,6 +168,7 @@ uploadApiRoutes.post("/", async (c)=>{
|
|
|
176
168
|
mode: "outer",
|
|
177
169
|
selector: "#upload-placeholder"
|
|
178
170
|
});
|
|
171
|
+
await stream.toast("Upload successful!");
|
|
179
172
|
});
|
|
180
173
|
}
|
|
181
174
|
// JSON response for API clients
|
|
@@ -190,7 +183,12 @@ uploadApiRoutes.post("/", async (c)=>{
|
|
|
190
183
|
} catch (err) {
|
|
191
184
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
192
185
|
console.error("Upload error:", err);
|
|
193
|
-
|
|
186
|
+
if (wantsSSE(c)) {
|
|
187
|
+
return sse(c, async (stream)=>{
|
|
188
|
+
await stream.remove("#upload-placeholder");
|
|
189
|
+
await stream.toast("Upload failed. Please try again.", "error");
|
|
190
|
+
});
|
|
191
|
+
}
|
|
194
192
|
return c.json({
|
|
195
193
|
error: "Upload failed"
|
|
196
194
|
}, 500);
|