@jant/core 0.2.12 → 0.2.13
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/bin/jant.js +3 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +112 -85
- package/dist/auth.d.ts +1 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -1
- package/dist/client.js +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/i18n/context.d.ts.map +1 -1
- package/dist/i18n/context.js +0 -3
- package/dist/i18n/detect.d.ts +0 -11
- package/dist/i18n/detect.d.ts.map +1 -1
- package/dist/i18n/detect.js +1 -52
- package/dist/i18n/i18n.d.ts +4 -14
- package/dist/i18n/i18n.d.ts.map +1 -1
- package/dist/i18n/i18n.js +19 -25
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/middleware.d.ts +2 -5
- package/dist/i18n/middleware.d.ts.map +1 -1
- package/dist/i18n/middleware.js +12 -23
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/image.d.ts.map +1 -1
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/sse.d.ts +45 -17
- package/dist/lib/sse.d.ts.map +1 -1
- package/dist/lib/sse.js +77 -37
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/routes/api/posts.js +0 -1
- package/dist/routes/api/upload.js +3 -1
- package/dist/routes/dash/collections.d.ts.map +1 -1
- package/dist/routes/dash/collections.js +134 -142
- package/dist/routes/dash/index.js +25 -26
- package/dist/routes/dash/media.d.ts.map +1 -1
- package/dist/routes/dash/media.js +60 -56
- package/dist/routes/dash/pages.js +64 -66
- package/dist/routes/dash/posts.d.ts.map +1 -1
- package/dist/routes/dash/posts.js +50 -59
- package/dist/routes/dash/redirects.d.ts.map +1 -1
- package/dist/routes/dash/redirects.js +63 -60
- package/dist/routes/dash/settings.d.ts.map +1 -1
- package/dist/routes/dash/settings.js +249 -93
- package/dist/routes/feed/rss.js +6 -4
- package/dist/routes/pages/archive.js +60 -62
- package/dist/routes/pages/collection.js +8 -8
- package/dist/routes/pages/home.js +14 -14
- package/dist/routes/pages/page.js +7 -6
- package/dist/routes/pages/post.js +8 -8
- package/dist/routes/pages/search.js +25 -27
- package/dist/services/collection.d.ts.map +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/media.d.ts.map +1 -1
- package/dist/services/post.d.ts.map +1 -1
- package/dist/services/redirect.d.ts.map +1 -1
- package/dist/services/settings.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.d.ts +1 -1
- package/dist/theme/components/ActionButtons.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.js +17 -21
- package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
- package/dist/theme/components/DangerZone.d.ts.map +1 -1
- package/dist/theme/components/DangerZone.js +12 -15
- package/dist/theme/components/EmptyState.d.ts.map +1 -1
- package/dist/theme/components/PageForm.d.ts.map +1 -1
- package/dist/theme/components/PageForm.js +58 -56
- package/dist/theme/components/Pagination.d.ts.map +1 -1
- package/dist/theme/components/Pagination.js +22 -25
- package/dist/theme/components/PostForm.d.ts +0 -1
- package/dist/theme/components/PostForm.d.ts.map +1 -1
- package/dist/theme/components/PostForm.js +85 -77
- package/dist/theme/components/PostList.d.ts.map +1 -1
- package/dist/theme/components/PostList.js +17 -17
- package/dist/theme/components/ThreadView.d.ts.map +1 -1
- package/dist/theme/components/ThreadView.js +15 -18
- package/dist/theme/components/TypeBadge.d.ts.map +1 -1
- package/dist/theme/components/TypeBadge.js +20 -20
- package/dist/theme/components/VisibilityBadge.d.ts.map +1 -1
- package/dist/theme/components/VisibilityBadge.js +14 -14
- package/dist/theme/components/index.d.ts +1 -1
- package/dist/theme/components/index.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.js +4 -2
- package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
- package/dist/theme/layouts/DashLayout.js +29 -29
- package/dist/types/lingui-react-macro.d.js +9 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vendor/datastar.js +1606 -0
- package/package.json +5 -2
- package/src/app.tsx +175 -56
- package/src/auth.ts +5 -1
- package/src/client.ts +1 -1
- package/src/db/schema.ts +22 -7
- package/src/i18n/EXAMPLES.md +34 -14
- package/src/i18n/README.md +19 -9
- package/src/i18n/context.tsx +1 -4
- package/src/i18n/detect.ts +1 -67
- package/src/i18n/i18n.ts +15 -19
- package/src/i18n/index.ts +0 -3
- package/src/i18n/middleware.ts +12 -24
- package/src/lib/constants.ts +2 -1
- package/src/lib/image-processor.ts +23 -7
- package/src/lib/image.ts +6 -2
- package/src/lib/schemas.ts +6 -2
- package/src/lib/sse.ts +138 -50
- package/src/middleware/auth.ts +6 -2
- package/src/routes/api/posts.ts +14 -5
- package/src/routes/api/upload.ts +25 -7
- package/src/routes/dash/collections.tsx +162 -70
- package/src/routes/dash/index.tsx +22 -7
- package/src/routes/dash/media.tsx +59 -16
- package/src/routes/dash/pages.tsx +102 -44
- package/src/routes/dash/posts.tsx +87 -54
- package/src/routes/dash/redirects.tsx +74 -26
- package/src/routes/dash/settings.tsx +250 -57
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/pages/archive.tsx +71 -21
- package/src/routes/pages/collection.tsx +21 -6
- package/src/routes/pages/home.tsx +30 -9
- package/src/routes/pages/page.tsx +14 -5
- package/src/routes/pages/post.tsx +21 -7
- package/src/routes/pages/search.tsx +42 -11
- package/src/services/collection.ts +34 -9
- package/src/services/index.ts +4 -1
- package/src/services/media.ts +15 -3
- package/src/services/post.ts +39 -10
- package/src/services/redirect.ts +4 -1
- package/src/services/settings.ts +14 -3
- package/src/theme/components/ActionButtons.tsx +26 -14
- package/src/theme/components/CrudPageHeader.tsx +6 -1
- package/src/theme/components/DangerZone.tsx +19 -13
- package/src/theme/components/EmptyState.tsx +6 -1
- package/src/theme/components/PageForm.tsx +71 -24
- package/src/theme/components/Pagination.tsx +26 -8
- package/src/theme/components/PostForm.tsx +72 -25
- package/src/theme/components/PostList.tsx +16 -5
- package/src/theme/components/ThreadView.tsx +25 -7
- package/src/theme/components/TypeBadge.tsx +13 -4
- package/src/theme/components/VisibilityBadge.tsx +17 -5
- package/src/theme/components/index.ts +4 -1
- package/src/theme/layouts/BaseLayout.tsx +5 -2
- package/src/theme/layouts/DashLayout.tsx +41 -12
- package/src/types/lingui-react-macro.d.ts +34 -0
- package/src/types.ts +16 -2
- package/src/vendor/datastar.js +9 -0
- package/src/vendor/datastar.js.map +7 -0
package/src/lib/sse.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Server-Sent Events (SSE) utilities for Datastar
|
|
2
|
+
* Server-Sent Events (SSE) utilities for Datastar v1.0.0-RC.7
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Datastar uses SSE for real-time UI updates without page reloads.
|
|
4
|
+
* Generates SSE events compatible with the Datastar client's expected format.
|
|
6
5
|
*
|
|
7
6
|
* @see https://data-star.dev/
|
|
8
7
|
*
|
|
@@ -11,7 +10,8 @@
|
|
|
11
10
|
* app.post("/api/example", (c) => {
|
|
12
11
|
* return sse(c, async (stream) => {
|
|
13
12
|
* await stream.patchSignals({ loading: false });
|
|
14
|
-
* await stream.patchElements("
|
|
13
|
+
* await stream.patchElements('<div id="result">Done!</div>');
|
|
14
|
+
* await stream.redirect("/success");
|
|
15
15
|
* });
|
|
16
16
|
* });
|
|
17
17
|
* ```
|
|
@@ -20,9 +20,19 @@
|
|
|
20
20
|
import type { Context } from "hono";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Patch modes for DOM updates
|
|
23
|
+
* Patch modes for DOM element updates
|
|
24
|
+
*
|
|
25
|
+
* @see https://data-star.dev/reference/action_plugins/backend/sse
|
|
24
26
|
*/
|
|
25
|
-
export type PatchMode =
|
|
27
|
+
export type PatchMode =
|
|
28
|
+
| "outer"
|
|
29
|
+
| "inner"
|
|
30
|
+
| "replace"
|
|
31
|
+
| "prepend"
|
|
32
|
+
| "append"
|
|
33
|
+
| "before"
|
|
34
|
+
| "after"
|
|
35
|
+
| "remove";
|
|
26
36
|
|
|
27
37
|
/**
|
|
28
38
|
* SSE stream writer for Datastar events
|
|
@@ -32,23 +42,27 @@ export interface SSEStream {
|
|
|
32
42
|
* Update reactive signals on the client
|
|
33
43
|
*
|
|
34
44
|
* @param signals - Object containing signal values to update
|
|
45
|
+
* @param options - Optional settings (e.g. onlyIfMissing)
|
|
35
46
|
*
|
|
36
47
|
* @example
|
|
37
48
|
* ```ts
|
|
38
49
|
* await stream.patchSignals({ count: 42, loading: false });
|
|
39
50
|
* ```
|
|
40
51
|
*/
|
|
41
|
-
patchSignals(
|
|
52
|
+
patchSignals(
|
|
53
|
+
signals: Record<string, unknown>,
|
|
54
|
+
options?: { onlyIfMissing?: boolean },
|
|
55
|
+
): void;
|
|
42
56
|
|
|
43
57
|
/**
|
|
44
|
-
* Update DOM elements
|
|
58
|
+
* Update DOM elements via patching
|
|
45
59
|
*
|
|
46
60
|
* @param html - HTML content (must include element with id for targeting)
|
|
47
|
-
* @param options - Optional mode and
|
|
61
|
+
* @param options - Optional patch mode, selector, and view transition
|
|
48
62
|
*
|
|
49
63
|
* @example
|
|
50
64
|
* ```ts
|
|
51
|
-
* //
|
|
65
|
+
* // Outer patch element with matching id (default)
|
|
52
66
|
* await stream.patchElements('<div id="content">New content</div>');
|
|
53
67
|
*
|
|
54
68
|
* // Append to a container
|
|
@@ -58,19 +72,56 @@ export interface SSEStream {
|
|
|
58
72
|
* });
|
|
59
73
|
* ```
|
|
60
74
|
*/
|
|
61
|
-
patchElements(
|
|
75
|
+
patchElements(
|
|
76
|
+
html: string,
|
|
77
|
+
options?: {
|
|
78
|
+
mode?: PatchMode;
|
|
79
|
+
selector?: string;
|
|
80
|
+
useViewTransition?: boolean;
|
|
81
|
+
},
|
|
82
|
+
): void;
|
|
62
83
|
|
|
63
84
|
/**
|
|
64
|
-
*
|
|
85
|
+
* Redirect the client to a new URL
|
|
65
86
|
*
|
|
66
|
-
*
|
|
87
|
+
* Uses patchElements internally to inject a script that navigates the client.
|
|
88
|
+
*
|
|
89
|
+
* @param url - The URL to redirect to
|
|
67
90
|
*
|
|
68
91
|
* @example
|
|
69
92
|
* ```ts
|
|
70
|
-
* await stream.
|
|
93
|
+
* await stream.redirect('/dash/posts');
|
|
71
94
|
* ```
|
|
72
95
|
*/
|
|
73
|
-
|
|
96
|
+
redirect(url: string): void;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Remove elements matching a CSS selector
|
|
100
|
+
*
|
|
101
|
+
* @param selector - CSS selector for elements to remove
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* await stream.remove('#placeholder');
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
remove(selector: string): void;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Format a single SSE event string
|
|
113
|
+
*
|
|
114
|
+
* @param eventType - The Datastar event type (e.g. "datastar-patch-elements")
|
|
115
|
+
* @param dataLines - Array of "key value" data lines
|
|
116
|
+
* @returns Formatted SSE event string
|
|
117
|
+
*/
|
|
118
|
+
function formatEvent(eventType: string, dataLines: readonly string[]): string {
|
|
119
|
+
let event = `event: ${eventType}\n`;
|
|
120
|
+
for (const line of dataLines) {
|
|
121
|
+
event += `data: ${line}\n`;
|
|
122
|
+
}
|
|
123
|
+
event += "\n";
|
|
124
|
+
return event;
|
|
74
125
|
}
|
|
75
126
|
|
|
76
127
|
/**
|
|
@@ -78,13 +129,13 @@ export interface SSEStream {
|
|
|
78
129
|
*
|
|
79
130
|
* @param c - Hono context
|
|
80
131
|
* @param handler - Async function that writes to the SSE stream
|
|
132
|
+
* @param options - Optional response options (e.g. headers for cookie forwarding)
|
|
81
133
|
* @returns Response with SSE content-type
|
|
82
134
|
*
|
|
83
135
|
* @example
|
|
84
136
|
* ```ts
|
|
85
137
|
* app.post("/api/upload", (c) => {
|
|
86
138
|
* return sse(c, async (stream) => {
|
|
87
|
-
* // Process upload...
|
|
88
139
|
* await stream.patchSignals({ uploading: false });
|
|
89
140
|
* await stream.patchElements('<div id="new-item">...</div>', {
|
|
90
141
|
* mode: 'append',
|
|
@@ -92,55 +143,92 @@ export interface SSEStream {
|
|
|
92
143
|
* });
|
|
93
144
|
* });
|
|
94
145
|
* });
|
|
146
|
+
*
|
|
147
|
+
* // With cookie forwarding (for auth)
|
|
148
|
+
* app.post("/signin", (c) => {
|
|
149
|
+
* return sse(c, async (stream) => {
|
|
150
|
+
* await stream.redirect('/dash');
|
|
151
|
+
* }, { headers: { 'Set-Cookie': cookieValue } });
|
|
152
|
+
* });
|
|
95
153
|
* ```
|
|
96
154
|
*/
|
|
97
|
-
export function sse(
|
|
155
|
+
export function sse(
|
|
156
|
+
c: Context,
|
|
157
|
+
handler: (stream: SSEStream) => Promise<void>,
|
|
158
|
+
options?: { headers?: Record<string, string> },
|
|
159
|
+
): Response {
|
|
98
160
|
const encoder = new TextEncoder();
|
|
99
161
|
|
|
100
|
-
const
|
|
162
|
+
const body = new ReadableStream({
|
|
101
163
|
async start(controller) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
164
|
+
const stream: SSEStream = {
|
|
165
|
+
patchSignals(signals, opts) {
|
|
166
|
+
const dataLines: string[] = [`signals ${JSON.stringify(signals)}`];
|
|
167
|
+
if (opts?.onlyIfMissing) {
|
|
168
|
+
dataLines.push("onlyIfMissing true");
|
|
169
|
+
}
|
|
170
|
+
controller.enqueue(
|
|
171
|
+
encoder.encode(formatEvent("datastar-patch-signals", dataLines)),
|
|
172
|
+
);
|
|
110
173
|
},
|
|
111
174
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
175
|
+
patchElements(html, opts) {
|
|
176
|
+
const dataLines: string[] = [];
|
|
177
|
+
// Each line of HTML gets its own "elements <line>" data line
|
|
178
|
+
for (const line of html.split("\n")) {
|
|
179
|
+
dataLines.push(`elements ${line}`);
|
|
116
180
|
}
|
|
117
|
-
if (
|
|
118
|
-
|
|
181
|
+
if (opts?.mode) {
|
|
182
|
+
dataLines.push(`mode ${opts.mode}`);
|
|
119
183
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
184
|
+
if (opts?.selector) {
|
|
185
|
+
dataLines.push(`selector ${opts.selector}`);
|
|
186
|
+
}
|
|
187
|
+
if (opts?.useViewTransition) {
|
|
188
|
+
dataLines.push("useViewTransition true");
|
|
189
|
+
}
|
|
190
|
+
controller.enqueue(
|
|
191
|
+
encoder.encode(formatEvent("datastar-patch-elements", dataLines)),
|
|
192
|
+
);
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
redirect(url) {
|
|
196
|
+
const escapedUrl = url.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
197
|
+
const script = `<script data-effect="el.remove()">window.location.href='${escapedUrl}'</script>`;
|
|
198
|
+
const dataLines: string[] = [
|
|
199
|
+
`elements ${script}`,
|
|
200
|
+
"mode append",
|
|
201
|
+
"selector body",
|
|
202
|
+
];
|
|
203
|
+
controller.enqueue(
|
|
204
|
+
encoder.encode(formatEvent("datastar-patch-elements", dataLines)),
|
|
205
|
+
);
|
|
123
206
|
},
|
|
124
207
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
208
|
+
remove(selector) {
|
|
209
|
+
controller.enqueue(
|
|
210
|
+
encoder.encode(
|
|
211
|
+
formatEvent("datastar-patch-elements", [
|
|
212
|
+
"elements ",
|
|
213
|
+
`mode remove`,
|
|
214
|
+
`selector ${selector}`,
|
|
215
|
+
]),
|
|
216
|
+
),
|
|
217
|
+
);
|
|
128
218
|
},
|
|
129
219
|
};
|
|
130
220
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
} finally {
|
|
134
|
-
controller.close();
|
|
135
|
-
}
|
|
221
|
+
await handler(stream);
|
|
222
|
+
controller.close();
|
|
136
223
|
},
|
|
137
224
|
});
|
|
138
225
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
226
|
+
const headers: Record<string, string> = {
|
|
227
|
+
"Content-Type": "text/event-stream",
|
|
228
|
+
"Cache-Control": "no-cache",
|
|
229
|
+
Connection: "keep-alive",
|
|
230
|
+
...options?.headers,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return new Response(body, { headers });
|
|
146
234
|
}
|
package/src/middleware/auth.ts
CHANGED
|
@@ -21,7 +21,9 @@ export function requireAuth(redirectTo = "/signin"): MiddlewareHandler<Env> {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
try {
|
|
24
|
-
const session = await c.var.auth.api.getSession({
|
|
24
|
+
const session = await c.var.auth.api.getSession({
|
|
25
|
+
headers: c.req.raw.headers,
|
|
26
|
+
});
|
|
25
27
|
|
|
26
28
|
if (!session?.user) {
|
|
27
29
|
return c.redirect(redirectTo);
|
|
@@ -45,7 +47,9 @@ export function requireAuthApi(): MiddlewareHandler<Env> {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
try {
|
|
48
|
-
const session = await c.var.auth.api.getSession({
|
|
50
|
+
const session = await c.var.auth.api.getSession({
|
|
51
|
+
headers: c.req.raw.headers,
|
|
52
|
+
});
|
|
49
53
|
|
|
50
54
|
if (!session?.user) {
|
|
51
55
|
return c.json({ error: "Unauthorized" }, 401);
|
package/src/routes/api/posts.ts
CHANGED
|
@@ -32,8 +32,9 @@ postsApiRoutes.get("/", async (c) => {
|
|
|
32
32
|
...p,
|
|
33
33
|
sqid: sqid.encode(p.id),
|
|
34
34
|
})),
|
|
35
|
-
|
|
36
|
-
nextCursor:
|
|
35
|
+
|
|
36
|
+
nextCursor:
|
|
37
|
+
posts.length === limit ? sqid.encode(posts[posts.length - 1]!.id) : null,
|
|
37
38
|
});
|
|
38
39
|
});
|
|
39
40
|
|
|
@@ -55,7 +56,10 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
55
56
|
// Validate request body
|
|
56
57
|
const parseResult = CreatePostSchema.safeParse(rawBody);
|
|
57
58
|
if (!parseResult.success) {
|
|
58
|
-
return c.json(
|
|
59
|
+
return c.json(
|
|
60
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
61
|
+
400,
|
|
62
|
+
);
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
const body = parseResult.data;
|
|
@@ -68,7 +72,9 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
68
72
|
sourceUrl: body.sourceUrl || undefined,
|
|
69
73
|
sourceName: body.sourceName,
|
|
70
74
|
path: body.path || undefined,
|
|
71
|
-
replyToId: body.replyToId
|
|
75
|
+
replyToId: body.replyToId
|
|
76
|
+
? (sqid.decode(body.replyToId) ?? undefined)
|
|
77
|
+
: undefined,
|
|
72
78
|
publishedAt: body.publishedAt,
|
|
73
79
|
});
|
|
74
80
|
|
|
@@ -85,7 +91,10 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
85
91
|
// Validate request body
|
|
86
92
|
const parseResult = UpdatePostSchema.safeParse(rawBody);
|
|
87
93
|
if (!parseResult.success) {
|
|
88
|
-
return c.json(
|
|
94
|
+
return c.json(
|
|
95
|
+
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
96
|
+
400,
|
|
97
|
+
);
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
const body = parseResult.data;
|
package/src/routes/api/upload.ts
CHANGED
|
@@ -33,7 +33,7 @@ function renderMediaCard(
|
|
|
33
33
|
size: number;
|
|
34
34
|
},
|
|
35
35
|
r2PublicUrl?: string,
|
|
36
|
-
imageTransformUrl?: string
|
|
36
|
+
imageTransformUrl?: string,
|
|
37
37
|
): string {
|
|
38
38
|
const fullUrl = getMediaUrl(media.id, media.r2Key, r2PublicUrl);
|
|
39
39
|
const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
|
|
@@ -79,7 +79,9 @@ function renderMediaCard(
|
|
|
79
79
|
href="/dash/media/${media.id}"
|
|
80
80
|
class="block aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary"
|
|
81
81
|
>
|
|
82
|
-
<div
|
|
82
|
+
<div
|
|
83
|
+
class="w-full h-full flex items-center justify-center text-muted-foreground"
|
|
84
|
+
>
|
|
83
85
|
<span class="text-xs">${media.mimeType}</span>
|
|
84
86
|
</div>
|
|
85
87
|
</a>
|
|
@@ -104,7 +106,9 @@ function formatSize(bytes: number): string {
|
|
|
104
106
|
/**
|
|
105
107
|
* Check if request wants SSE response (from Datastar)
|
|
106
108
|
*/
|
|
107
|
-
function wantsSSE(c: {
|
|
109
|
+
function wantsSSE(c: {
|
|
110
|
+
req: { header: (name: string) => string | undefined };
|
|
111
|
+
}): boolean {
|
|
108
112
|
const accept = c.req.header("accept") || "";
|
|
109
113
|
return accept.includes("text/event-stream");
|
|
110
114
|
}
|
|
@@ -114,7 +118,9 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
114
118
|
if (!c.env.R2) {
|
|
115
119
|
if (wantsSSE(c)) {
|
|
116
120
|
return sse(c, async (stream) => {
|
|
117
|
-
await stream.patchSignals({
|
|
121
|
+
await stream.patchSignals({
|
|
122
|
+
_uploadError: "R2 storage not configured",
|
|
123
|
+
});
|
|
118
124
|
});
|
|
119
125
|
}
|
|
120
126
|
return c.json({ error: "R2 storage not configured" }, 500);
|
|
@@ -133,7 +139,13 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
// Validate file type
|
|
136
|
-
const allowedTypes = [
|
|
142
|
+
const allowedTypes = [
|
|
143
|
+
"image/jpeg",
|
|
144
|
+
"image/png",
|
|
145
|
+
"image/gif",
|
|
146
|
+
"image/webp",
|
|
147
|
+
"image/svg+xml",
|
|
148
|
+
];
|
|
137
149
|
if (!allowedTypes.includes(file.type)) {
|
|
138
150
|
if (wantsSSE(c)) {
|
|
139
151
|
return sse(c, async (stream) => {
|
|
@@ -148,7 +160,9 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
148
160
|
if (file.size > maxSize) {
|
|
149
161
|
if (wantsSSE(c)) {
|
|
150
162
|
return sse(c, async (stream) => {
|
|
151
|
-
await stream.patchSignals({
|
|
163
|
+
await stream.patchSignals({
|
|
164
|
+
_uploadError: "File too large (max 10MB)",
|
|
165
|
+
});
|
|
152
166
|
});
|
|
153
167
|
}
|
|
154
168
|
return c.json({ error: "File too large (max 10MB)" }, 400);
|
|
@@ -180,7 +194,11 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
180
194
|
|
|
181
195
|
// SSE response for Datastar
|
|
182
196
|
if (wantsSSE(c)) {
|
|
183
|
-
const cardHtml = renderMediaCard(
|
|
197
|
+
const cardHtml = renderMediaCard(
|
|
198
|
+
media,
|
|
199
|
+
c.env.R2_PUBLIC_URL,
|
|
200
|
+
c.env.IMAGE_TRANSFORM_URL,
|
|
201
|
+
);
|
|
184
202
|
|
|
185
203
|
return sse(c, async (stream) => {
|
|
186
204
|
// Replace placeholder with real media card
|