@jant/core 0.2.11 → 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/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/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 +13 -3
- 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 -25
- package/dist/routes/dash/media.d.ts.map +1 -1
- package/dist/routes/dash/media.js +60 -56
- package/dist/routes/dash/pages.d.ts.map +1 -1
- 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 +2 -2
- 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 +7 -15
- package/src/app.tsx +222 -59
- package/src/auth.ts +5 -1
- package/src/client.ts +1 -1
- package/src/db/migrations/meta/0000_snapshot.json +16 -47
- package/src/db/migrations/meta/_journal.json +1 -1
- package/src/db/schema.ts +22 -7
- package/src/i18n/EXAMPLES.md +45 -23
- package/src/i18n/README.md +39 -25
- 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 +14 -6
- package/src/lib/image.ts +2 -2
- package/src/lib/schemas.ts +7 -3
- package/src/lib/sse.ts +133 -51
- package/src/middleware/auth.ts +6 -2
- package/src/routes/api/posts.ts +9 -9
- package/src/routes/api/upload.ts +39 -10
- package/src/routes/dash/collections.tsx +249 -81
- package/src/routes/dash/index.tsx +22 -7
- package/src/routes/dash/media.tsx +94 -24
- package/src/routes/dash/pages.tsx +132 -54
- package/src/routes/dash/posts.tsx +99 -57
- package/src/routes/dash/redirects.tsx +117 -36
- package/src/routes/dash/settings.tsx +268 -55
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/pages/archive.tsx +78 -24
- package/src/routes/pages/collection.tsx +32 -8
- package/src/routes/pages/home.tsx +38 -10
- package/src/routes/pages/page.tsx +15 -5
- package/src/routes/pages/post.tsx +17 -6
- package/src/routes/pages/search.tsx +50 -13
- package/src/services/collection.ts +29 -8
- package/src/services/index.ts +4 -1
- package/src/services/media.ts +15 -3
- package/src/services/post.ts +37 -10
- package/src/services/redirect.ts +4 -1
- package/src/services/settings.ts +14 -3
- package/src/theme/components/ActionButtons.tsx +31 -15
- package/src/theme/components/CrudPageHeader.tsx +3 -4
- package/src/theme/components/DangerZone.tsx +19 -13
- package/src/theme/components/EmptyState.tsx +1 -5
- package/src/theme/components/PageForm.tsx +80 -25
- package/src/theme/components/Pagination.tsx +34 -31
- package/src/theme/components/PostForm.tsx +91 -27
- package/src/theme/components/PostList.tsx +23 -6
- package/src/theme/components/ThreadView.tsx +25 -10
- package/src/theme/components/TypeBadge.tsx +13 -4
- package/src/theme/components/VisibilityBadge.tsx +17 -5
- package/src/theme/components/index.ts +12 -2
- package/src/theme/layouts/BaseLayout.tsx +6 -5
- package/src/theme/layouts/DashLayout.tsx +71 -18
- package/src/types/lingui-react-macro.d.ts +34 -0
- package/src/types.ts +16 -4
- package/src/vendor/datastar.js +9 -0
- package/src/vendor/datastar.js.map +7 -0
- package/dist/plugin.d.ts +0 -3
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -20
- package/dist/tailwind.d.ts +0 -12
- package/dist/tailwind.d.ts.map +0 -1
- package/dist/tailwind.js +0 -15
|
@@ -115,7 +115,7 @@ function calculateDimensions(
|
|
|
115
115
|
width: number,
|
|
116
116
|
height: number,
|
|
117
117
|
maxWidth: number,
|
|
118
|
-
maxHeight: number
|
|
118
|
+
maxHeight: number,
|
|
119
119
|
): { width: number; height: number } {
|
|
120
120
|
if (width <= maxWidth && height <= maxHeight) {
|
|
121
121
|
return { width, height };
|
|
@@ -131,7 +131,10 @@ function calculateDimensions(
|
|
|
131
131
|
/**
|
|
132
132
|
* Process image file
|
|
133
133
|
*/
|
|
134
|
-
async function process(
|
|
134
|
+
async function process(
|
|
135
|
+
file: File,
|
|
136
|
+
options: ProcessOptions = {},
|
|
137
|
+
): Promise<Blob> {
|
|
135
138
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
136
139
|
|
|
137
140
|
// Read file buffer for EXIF
|
|
@@ -152,7 +155,7 @@ async function process(file: File, options: ProcessOptions = {}): Promise<Blob>
|
|
|
152
155
|
srcWidth,
|
|
153
156
|
srcHeight,
|
|
154
157
|
opts.maxWidth,
|
|
155
|
-
opts.maxHeight
|
|
158
|
+
opts.maxHeight,
|
|
156
159
|
);
|
|
157
160
|
|
|
158
161
|
// Create canvas
|
|
@@ -192,7 +195,7 @@ async function process(file: File, options: ProcessOptions = {}): Promise<Blob>
|
|
|
192
195
|
}
|
|
193
196
|
},
|
|
194
197
|
opts.mimeType,
|
|
195
|
-
opts.quality
|
|
198
|
+
opts.quality,
|
|
196
199
|
);
|
|
197
200
|
});
|
|
198
201
|
}
|
|
@@ -200,7 +203,10 @@ async function process(file: File, options: ProcessOptions = {}): Promise<Blob>
|
|
|
200
203
|
/**
|
|
201
204
|
* Process file and create a new File object
|
|
202
205
|
*/
|
|
203
|
-
async function processToFile(
|
|
206
|
+
async function processToFile(
|
|
207
|
+
file: File,
|
|
208
|
+
options: ProcessOptions = {},
|
|
209
|
+
): Promise<File> {
|
|
204
210
|
const blob = await process(file, options);
|
|
205
211
|
|
|
206
212
|
// Generate new filename with .webp extension
|
|
@@ -214,5 +220,7 @@ export const ImageProcessor = { process, processToFile };
|
|
|
214
220
|
|
|
215
221
|
// Expose globally for inline scripts
|
|
216
222
|
if (typeof window !== "undefined") {
|
|
217
|
-
(
|
|
223
|
+
(
|
|
224
|
+
window as unknown as { ImageProcessor: typeof ImageProcessor }
|
|
225
|
+
).ImageProcessor = ImageProcessor;
|
|
218
226
|
}
|
package/src/lib/image.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface ImageOptions {
|
|
|
51
51
|
export function getImageUrl(
|
|
52
52
|
originalUrl: string,
|
|
53
53
|
transformUrl?: string,
|
|
54
|
-
options?: ImageOptions
|
|
54
|
+
options?: ImageOptions,
|
|
55
55
|
): string {
|
|
56
56
|
if (!transformUrl || !options || Object.keys(options).length === 0) {
|
|
57
57
|
return originalUrl;
|
|
@@ -96,7 +96,7 @@ export function getImageUrl(
|
|
|
96
96
|
export function getMediaUrl(
|
|
97
97
|
mediaId: string,
|
|
98
98
|
r2Key: string,
|
|
99
|
-
r2PublicUrl?: string
|
|
99
|
+
r2PublicUrl?: string,
|
|
100
100
|
): string {
|
|
101
101
|
if (r2PublicUrl) {
|
|
102
102
|
return `${r2PublicUrl}/${r2Key}`;
|
package/src/lib/schemas.ts
CHANGED
|
@@ -39,7 +39,11 @@ export const CreatePostSchema = z.object({
|
|
|
39
39
|
visibility: VisibilitySchema,
|
|
40
40
|
sourceUrl: z.string().url().optional().or(z.literal("")),
|
|
41
41
|
sourceName: z.string().optional(),
|
|
42
|
-
path: z
|
|
42
|
+
path: z
|
|
43
|
+
.string()
|
|
44
|
+
.regex(/^[a-z0-9-]*$/)
|
|
45
|
+
.optional()
|
|
46
|
+
.or(z.literal("")),
|
|
43
47
|
replyToId: z.string().optional(), // Sqid format
|
|
44
48
|
publishedAt: z.number().int().positive().optional(),
|
|
45
49
|
});
|
|
@@ -61,7 +65,7 @@ export const UpdatePostSchema = CreatePostSchema.partial();
|
|
|
61
65
|
export function parseFormData<T>(
|
|
62
66
|
formData: FormData,
|
|
63
67
|
key: string,
|
|
64
|
-
schema: z.ZodSchema<T
|
|
68
|
+
schema: z.ZodSchema<T>,
|
|
65
69
|
): T {
|
|
66
70
|
const value = formData.get(key);
|
|
67
71
|
if (value === null) {
|
|
@@ -82,7 +86,7 @@ export function parseFormData<T>(
|
|
|
82
86
|
export function parseFormDataOptional<T>(
|
|
83
87
|
formData: FormData,
|
|
84
88
|
key: string,
|
|
85
|
-
schema: z.ZodSchema<T
|
|
89
|
+
schema: z.ZodSchema<T>,
|
|
86
90
|
): T | undefined {
|
|
87
91
|
const value = formData.get(key);
|
|
88
92
|
if (value === null || value === "") {
|
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
|
|
@@ -60,20 +74,54 @@ export interface SSEStream {
|
|
|
60
74
|
*/
|
|
61
75
|
patchElements(
|
|
62
76
|
html: string,
|
|
63
|
-
options?: {
|
|
64
|
-
|
|
77
|
+
options?: {
|
|
78
|
+
mode?: PatchMode;
|
|
79
|
+
selector?: string;
|
|
80
|
+
useViewTransition?: boolean;
|
|
81
|
+
},
|
|
82
|
+
): void;
|
|
65
83
|
|
|
66
84
|
/**
|
|
67
|
-
*
|
|
85
|
+
* Redirect the client to a new URL
|
|
68
86
|
*
|
|
69
|
-
*
|
|
87
|
+
* Uses patchElements internally to inject a script that navigates the client.
|
|
88
|
+
*
|
|
89
|
+
* @param url - The URL to redirect to
|
|
70
90
|
*
|
|
71
91
|
* @example
|
|
72
92
|
* ```ts
|
|
73
|
-
* await stream.
|
|
93
|
+
* await stream.redirect('/dash/posts');
|
|
74
94
|
* ```
|
|
75
95
|
*/
|
|
76
|
-
|
|
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;
|
|
77
125
|
}
|
|
78
126
|
|
|
79
127
|
/**
|
|
@@ -81,13 +129,13 @@ export interface SSEStream {
|
|
|
81
129
|
*
|
|
82
130
|
* @param c - Hono context
|
|
83
131
|
* @param handler - Async function that writes to the SSE stream
|
|
132
|
+
* @param options - Optional response options (e.g. headers for cookie forwarding)
|
|
84
133
|
* @returns Response with SSE content-type
|
|
85
134
|
*
|
|
86
135
|
* @example
|
|
87
136
|
* ```ts
|
|
88
137
|
* app.post("/api/upload", (c) => {
|
|
89
138
|
* return sse(c, async (stream) => {
|
|
90
|
-
* // Process upload...
|
|
91
139
|
* await stream.patchSignals({ uploading: false });
|
|
92
140
|
* await stream.patchElements('<div id="new-item">...</div>', {
|
|
93
141
|
* mode: 'append',
|
|
@@ -95,58 +143,92 @@ export interface SSEStream {
|
|
|
95
143
|
* });
|
|
96
144
|
* });
|
|
97
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
|
+
* });
|
|
98
153
|
* ```
|
|
99
154
|
*/
|
|
100
155
|
export function sse(
|
|
101
156
|
c: Context,
|
|
102
|
-
handler: (stream: SSEStream) => Promise<void
|
|
157
|
+
handler: (stream: SSEStream) => Promise<void>,
|
|
158
|
+
options?: { headers?: Record<string, string> },
|
|
103
159
|
): Response {
|
|
104
160
|
const encoder = new TextEncoder();
|
|
105
161
|
|
|
106
|
-
const
|
|
162
|
+
const body = new ReadableStream({
|
|
107
163
|
async start(controller) {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
);
|
|
116
173
|
},
|
|
117
174
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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}`);
|
|
122
180
|
}
|
|
123
|
-
if (
|
|
124
|
-
|
|
181
|
+
if (opts?.mode) {
|
|
182
|
+
dataLines.push(`mode ${opts.mode}`);
|
|
125
183
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
);
|
|
129
206
|
},
|
|
130
207
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
);
|
|
134
218
|
},
|
|
135
219
|
};
|
|
136
220
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
} finally {
|
|
140
|
-
controller.close();
|
|
141
|
-
}
|
|
221
|
+
await handler(stream);
|
|
222
|
+
controller.close();
|
|
142
223
|
},
|
|
143
224
|
});
|
|
144
225
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 });
|
|
152
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
|
@@ -23,7 +23,7 @@ postsApiRoutes.get("/", async (c) => {
|
|
|
23
23
|
const posts = await c.var.services.posts.list({
|
|
24
24
|
type,
|
|
25
25
|
visibility: visibility ? [visibility] : ["featured", "quiet"],
|
|
26
|
-
cursor: cursor ? sqid.decode(cursor) ?? undefined : undefined,
|
|
26
|
+
cursor: cursor ? (sqid.decode(cursor) ?? undefined) : undefined,
|
|
27
27
|
limit,
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -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
|
|
|
@@ -50,7 +51,6 @@ postsApiRoutes.get("/:id", async (c) => {
|
|
|
50
51
|
|
|
51
52
|
// Create post (requires auth)
|
|
52
53
|
postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
53
|
-
|
|
54
54
|
const rawBody = await c.req.json();
|
|
55
55
|
|
|
56
56
|
// Validate request body
|
|
@@ -58,7 +58,7 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
58
58
|
if (!parseResult.success) {
|
|
59
59
|
return c.json(
|
|
60
60
|
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
61
|
-
400
|
|
61
|
+
400,
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -72,7 +72,9 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
72
72
|
sourceUrl: body.sourceUrl || undefined,
|
|
73
73
|
sourceName: body.sourceName,
|
|
74
74
|
path: body.path || undefined,
|
|
75
|
-
replyToId: body.replyToId
|
|
75
|
+
replyToId: body.replyToId
|
|
76
|
+
? (sqid.decode(body.replyToId) ?? undefined)
|
|
77
|
+
: undefined,
|
|
76
78
|
publishedAt: body.publishedAt,
|
|
77
79
|
});
|
|
78
80
|
|
|
@@ -81,7 +83,6 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
81
83
|
|
|
82
84
|
// Update post (requires auth)
|
|
83
85
|
postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
84
|
-
|
|
85
86
|
const id = sqid.decode(c.req.param("id"));
|
|
86
87
|
if (!id) return c.json({ error: "Invalid ID" }, 400);
|
|
87
88
|
|
|
@@ -92,7 +93,7 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
92
93
|
if (!parseResult.success) {
|
|
93
94
|
return c.json(
|
|
94
95
|
{ error: "Validation failed", details: parseResult.error.flatten() },
|
|
95
|
-
400
|
|
96
|
+
400,
|
|
96
97
|
);
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -116,7 +117,6 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
116
117
|
|
|
117
118
|
// Delete post (requires auth)
|
|
118
119
|
postsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
119
|
-
|
|
120
120
|
const id = sqid.decode(c.req.param("id"));
|
|
121
121
|
if (!id) return c.json({ error: "Invalid ID" }, 400);
|
|
122
122
|
|
package/src/routes/api/upload.ts
CHANGED
|
@@ -24,9 +24,16 @@ uploadApiRoutes.use("*", requireAuthApi());
|
|
|
24
24
|
* Render a media card HTML string for SSE response
|
|
25
25
|
*/
|
|
26
26
|
function renderMediaCard(
|
|
27
|
-
media: {
|
|
27
|
+
media: {
|
|
28
|
+
id: string;
|
|
29
|
+
r2Key: string;
|
|
30
|
+
mimeType: string;
|
|
31
|
+
originalName: string;
|
|
32
|
+
alt: string | null;
|
|
33
|
+
size: number;
|
|
34
|
+
},
|
|
28
35
|
r2PublicUrl?: string,
|
|
29
|
-
imageTransformUrl?: string
|
|
36
|
+
imageTransformUrl?: string,
|
|
30
37
|
): string {
|
|
31
38
|
const fullUrl = getMediaUrl(media.id, media.r2Key, r2PublicUrl);
|
|
32
39
|
const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
|
|
@@ -54,7 +61,11 @@ function renderMediaCard(
|
|
|
54
61
|
loading="lazy"
|
|
55
62
|
/>
|
|
56
63
|
</button>
|
|
57
|
-
<a
|
|
64
|
+
<a
|
|
65
|
+
href="/dash/media/${media.id}"
|
|
66
|
+
class="block mt-2 text-xs truncate hover:underline"
|
|
67
|
+
title="${media.originalName}"
|
|
68
|
+
>
|
|
58
69
|
${media.originalName}
|
|
59
70
|
</a>
|
|
60
71
|
<div class="text-xs text-muted-foreground">${sizeStr}</div>
|
|
@@ -68,11 +79,17 @@ function renderMediaCard(
|
|
|
68
79
|
href="/dash/media/${media.id}"
|
|
69
80
|
class="block aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary"
|
|
70
81
|
>
|
|
71
|
-
<div
|
|
82
|
+
<div
|
|
83
|
+
class="w-full h-full flex items-center justify-center text-muted-foreground"
|
|
84
|
+
>
|
|
72
85
|
<span class="text-xs">${media.mimeType}</span>
|
|
73
86
|
</div>
|
|
74
87
|
</a>
|
|
75
|
-
<a
|
|
88
|
+
<a
|
|
89
|
+
href="/dash/media/${media.id}"
|
|
90
|
+
class="block mt-2 text-xs truncate hover:underline"
|
|
91
|
+
title="${media.originalName}"
|
|
92
|
+
>
|
|
76
93
|
${media.originalName}
|
|
77
94
|
</a>
|
|
78
95
|
<div class="text-xs text-muted-foreground">${sizeStr}</div>
|
|
@@ -89,7 +106,9 @@ function formatSize(bytes: number): string {
|
|
|
89
106
|
/**
|
|
90
107
|
* Check if request wants SSE response (from Datastar)
|
|
91
108
|
*/
|
|
92
|
-
function wantsSSE(c: {
|
|
109
|
+
function wantsSSE(c: {
|
|
110
|
+
req: { header: (name: string) => string | undefined };
|
|
111
|
+
}): boolean {
|
|
93
112
|
const accept = c.req.header("accept") || "";
|
|
94
113
|
return accept.includes("text/event-stream");
|
|
95
114
|
}
|
|
@@ -99,7 +118,9 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
99
118
|
if (!c.env.R2) {
|
|
100
119
|
if (wantsSSE(c)) {
|
|
101
120
|
return sse(c, async (stream) => {
|
|
102
|
-
await stream.patchSignals({
|
|
121
|
+
await stream.patchSignals({
|
|
122
|
+
_uploadError: "R2 storage not configured",
|
|
123
|
+
});
|
|
103
124
|
});
|
|
104
125
|
}
|
|
105
126
|
return c.json({ error: "R2 storage not configured" }, 500);
|
|
@@ -118,7 +139,13 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
118
139
|
}
|
|
119
140
|
|
|
120
141
|
// Validate file type
|
|
121
|
-
const allowedTypes = [
|
|
142
|
+
const allowedTypes = [
|
|
143
|
+
"image/jpeg",
|
|
144
|
+
"image/png",
|
|
145
|
+
"image/gif",
|
|
146
|
+
"image/webp",
|
|
147
|
+
"image/svg+xml",
|
|
148
|
+
];
|
|
122
149
|
if (!allowedTypes.includes(file.type)) {
|
|
123
150
|
if (wantsSSE(c)) {
|
|
124
151
|
return sse(c, async (stream) => {
|
|
@@ -133,7 +160,9 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
133
160
|
if (file.size > maxSize) {
|
|
134
161
|
if (wantsSSE(c)) {
|
|
135
162
|
return sse(c, async (stream) => {
|
|
136
|
-
await stream.patchSignals({
|
|
163
|
+
await stream.patchSignals({
|
|
164
|
+
_uploadError: "File too large (max 10MB)",
|
|
165
|
+
});
|
|
137
166
|
});
|
|
138
167
|
}
|
|
139
168
|
return c.json({ error: "File too large (max 10MB)" }, 400);
|
|
@@ -168,7 +197,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
168
197
|
const cardHtml = renderMediaCard(
|
|
169
198
|
media,
|
|
170
199
|
c.env.R2_PUBLIC_URL,
|
|
171
|
-
c.env.IMAGE_TRANSFORM_URL
|
|
200
|
+
c.env.IMAGE_TRANSFORM_URL,
|
|
172
201
|
);
|
|
173
202
|
|
|
174
203
|
return sse(c, async (stream) => {
|