@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.
Files changed (153) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +112 -85
  3. package/dist/auth.d.ts +1 -0
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +2 -1
  6. package/dist/client.js +1 -1
  7. package/dist/db/schema.d.ts.map +1 -1
  8. package/dist/i18n/context.d.ts.map +1 -1
  9. package/dist/i18n/context.js +0 -3
  10. package/dist/i18n/detect.d.ts +0 -11
  11. package/dist/i18n/detect.d.ts.map +1 -1
  12. package/dist/i18n/detect.js +1 -52
  13. package/dist/i18n/i18n.d.ts +4 -14
  14. package/dist/i18n/i18n.d.ts.map +1 -1
  15. package/dist/i18n/i18n.js +19 -25
  16. package/dist/i18n/index.d.ts +1 -1
  17. package/dist/i18n/index.d.ts.map +1 -1
  18. package/dist/i18n/index.js +1 -1
  19. package/dist/i18n/middleware.d.ts +2 -5
  20. package/dist/i18n/middleware.d.ts.map +1 -1
  21. package/dist/i18n/middleware.js +12 -23
  22. package/dist/lib/constants.d.ts.map +1 -1
  23. package/dist/lib/schemas.d.ts.map +1 -1
  24. package/dist/lib/sse.d.ts +45 -17
  25. package/dist/lib/sse.d.ts.map +1 -1
  26. package/dist/lib/sse.js +77 -37
  27. package/dist/middleware/auth.d.ts.map +1 -1
  28. package/dist/routes/api/posts.js +0 -1
  29. package/dist/routes/api/upload.js +13 -3
  30. package/dist/routes/dash/collections.d.ts.map +1 -1
  31. package/dist/routes/dash/collections.js +134 -142
  32. package/dist/routes/dash/index.js +25 -25
  33. package/dist/routes/dash/media.d.ts.map +1 -1
  34. package/dist/routes/dash/media.js +60 -56
  35. package/dist/routes/dash/pages.d.ts.map +1 -1
  36. package/dist/routes/dash/pages.js +64 -66
  37. package/dist/routes/dash/posts.d.ts.map +1 -1
  38. package/dist/routes/dash/posts.js +50 -59
  39. package/dist/routes/dash/redirects.d.ts.map +1 -1
  40. package/dist/routes/dash/redirects.js +63 -60
  41. package/dist/routes/dash/settings.d.ts.map +1 -1
  42. package/dist/routes/dash/settings.js +249 -93
  43. package/dist/routes/feed/rss.js +6 -4
  44. package/dist/routes/pages/archive.js +60 -62
  45. package/dist/routes/pages/collection.js +8 -8
  46. package/dist/routes/pages/home.js +14 -14
  47. package/dist/routes/pages/page.js +7 -6
  48. package/dist/routes/pages/post.js +8 -8
  49. package/dist/routes/pages/search.js +25 -27
  50. package/dist/services/collection.d.ts.map +1 -1
  51. package/dist/services/index.d.ts.map +1 -1
  52. package/dist/services/media.d.ts.map +1 -1
  53. package/dist/services/post.d.ts.map +1 -1
  54. package/dist/services/redirect.d.ts.map +1 -1
  55. package/dist/services/settings.d.ts.map +1 -1
  56. package/dist/theme/components/ActionButtons.d.ts +1 -1
  57. package/dist/theme/components/ActionButtons.d.ts.map +1 -1
  58. package/dist/theme/components/ActionButtons.js +17 -21
  59. package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
  60. package/dist/theme/components/DangerZone.d.ts.map +1 -1
  61. package/dist/theme/components/DangerZone.js +12 -15
  62. package/dist/theme/components/EmptyState.d.ts.map +1 -1
  63. package/dist/theme/components/PageForm.d.ts.map +1 -1
  64. package/dist/theme/components/PageForm.js +58 -56
  65. package/dist/theme/components/Pagination.d.ts.map +1 -1
  66. package/dist/theme/components/Pagination.js +22 -25
  67. package/dist/theme/components/PostForm.d.ts +0 -1
  68. package/dist/theme/components/PostForm.d.ts.map +1 -1
  69. package/dist/theme/components/PostForm.js +85 -77
  70. package/dist/theme/components/PostList.d.ts.map +1 -1
  71. package/dist/theme/components/PostList.js +17 -17
  72. package/dist/theme/components/ThreadView.d.ts.map +1 -1
  73. package/dist/theme/components/ThreadView.js +15 -18
  74. package/dist/theme/components/TypeBadge.d.ts.map +1 -1
  75. package/dist/theme/components/TypeBadge.js +20 -20
  76. package/dist/theme/components/VisibilityBadge.d.ts.map +1 -1
  77. package/dist/theme/components/VisibilityBadge.js +14 -14
  78. package/dist/theme/components/index.d.ts +2 -2
  79. package/dist/theme/components/index.d.ts.map +1 -1
  80. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
  81. package/dist/theme/layouts/BaseLayout.js +4 -2
  82. package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
  83. package/dist/theme/layouts/DashLayout.js +29 -29
  84. package/dist/types/lingui-react-macro.d.js +9 -0
  85. package/dist/types.d.ts +2 -0
  86. package/dist/types.d.ts.map +1 -1
  87. package/dist/vendor/datastar.js +1606 -0
  88. package/package.json +7 -15
  89. package/src/app.tsx +222 -59
  90. package/src/auth.ts +5 -1
  91. package/src/client.ts +1 -1
  92. package/src/db/migrations/meta/0000_snapshot.json +16 -47
  93. package/src/db/migrations/meta/_journal.json +1 -1
  94. package/src/db/schema.ts +22 -7
  95. package/src/i18n/EXAMPLES.md +45 -23
  96. package/src/i18n/README.md +39 -25
  97. package/src/i18n/context.tsx +1 -4
  98. package/src/i18n/detect.ts +1 -67
  99. package/src/i18n/i18n.ts +15 -19
  100. package/src/i18n/index.ts +0 -3
  101. package/src/i18n/middleware.ts +12 -24
  102. package/src/lib/constants.ts +2 -1
  103. package/src/lib/image-processor.ts +14 -6
  104. package/src/lib/image.ts +2 -2
  105. package/src/lib/schemas.ts +7 -3
  106. package/src/lib/sse.ts +133 -51
  107. package/src/middleware/auth.ts +6 -2
  108. package/src/routes/api/posts.ts +9 -9
  109. package/src/routes/api/upload.ts +39 -10
  110. package/src/routes/dash/collections.tsx +249 -81
  111. package/src/routes/dash/index.tsx +22 -7
  112. package/src/routes/dash/media.tsx +94 -24
  113. package/src/routes/dash/pages.tsx +132 -54
  114. package/src/routes/dash/posts.tsx +99 -57
  115. package/src/routes/dash/redirects.tsx +117 -36
  116. package/src/routes/dash/settings.tsx +268 -55
  117. package/src/routes/feed/rss.ts +6 -4
  118. package/src/routes/pages/archive.tsx +78 -24
  119. package/src/routes/pages/collection.tsx +32 -8
  120. package/src/routes/pages/home.tsx +38 -10
  121. package/src/routes/pages/page.tsx +15 -5
  122. package/src/routes/pages/post.tsx +17 -6
  123. package/src/routes/pages/search.tsx +50 -13
  124. package/src/services/collection.ts +29 -8
  125. package/src/services/index.ts +4 -1
  126. package/src/services/media.ts +15 -3
  127. package/src/services/post.ts +37 -10
  128. package/src/services/redirect.ts +4 -1
  129. package/src/services/settings.ts +14 -3
  130. package/src/theme/components/ActionButtons.tsx +31 -15
  131. package/src/theme/components/CrudPageHeader.tsx +3 -4
  132. package/src/theme/components/DangerZone.tsx +19 -13
  133. package/src/theme/components/EmptyState.tsx +1 -5
  134. package/src/theme/components/PageForm.tsx +80 -25
  135. package/src/theme/components/Pagination.tsx +34 -31
  136. package/src/theme/components/PostForm.tsx +91 -27
  137. package/src/theme/components/PostList.tsx +23 -6
  138. package/src/theme/components/ThreadView.tsx +25 -10
  139. package/src/theme/components/TypeBadge.tsx +13 -4
  140. package/src/theme/components/VisibilityBadge.tsx +17 -5
  141. package/src/theme/components/index.ts +12 -2
  142. package/src/theme/layouts/BaseLayout.tsx +6 -5
  143. package/src/theme/layouts/DashLayout.tsx +71 -18
  144. package/src/types/lingui-react-macro.d.ts +34 -0
  145. package/src/types.ts +16 -4
  146. package/src/vendor/datastar.js +9 -0
  147. package/src/vendor/datastar.js.map +7 -0
  148. package/dist/plugin.d.ts +0 -3
  149. package/dist/plugin.d.ts.map +0 -1
  150. package/dist/plugin.js +0 -20
  151. package/dist/tailwind.d.ts +0 -12
  152. package/dist/tailwind.d.ts.map +0 -1
  153. 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(file: File, options: ProcessOptions = {}): Promise<Blob> {
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(file: File, options: ProcessOptions = {}): Promise<File> {
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
- (window as unknown as { ImageProcessor: typeof ImageProcessor }).ImageProcessor = ImageProcessor;
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}`;
@@ -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.string().regex(/^[a-z0-9-]*$/).optional().or(z.literal("")),
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
- * Provides helpers for streaming SSE responses that Datastar can consume.
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("#result", "<div>Done!</div>");
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 = "morph" | "inner" | "outer" | "append" | "prepend" | "remove";
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(signals: Record<string, unknown>): Promise<void>;
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 selector
61
+ * @param options - Optional patch mode, selector, and view transition
48
62
  *
49
63
  * @example
50
64
  * ```ts
51
- * // Replace element with matching id (default: morph)
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?: { mode?: PatchMode; selector?: string }
64
- ): Promise<void>;
77
+ options?: {
78
+ mode?: PatchMode;
79
+ selector?: string;
80
+ useViewTransition?: boolean;
81
+ },
82
+ ): void;
65
83
 
66
84
  /**
67
- * Execute JavaScript on the client
85
+ * Redirect the client to a new URL
68
86
  *
69
- * @param script - JavaScript code to execute
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.executeScript('console.log("Hello from server")');
93
+ * await stream.redirect('/dash/posts');
74
94
  * ```
75
95
  */
76
- executeScript(script: string): Promise<void>;
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 stream = new ReadableStream({
162
+ const body = new ReadableStream({
107
163
  async start(controller) {
108
- const write = (data: string) => {
109
- controller.enqueue(encoder.encode(data));
110
- };
111
-
112
- const sseStream: SSEStream = {
113
- async patchSignals(signals) {
114
- write(`event: datastar-patch-signals\n`);
115
- write(`data: signals ${JSON.stringify(signals)}\n\n`);
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
- async patchElements(html, options = {}) {
119
- write(`event: datastar-patch-elements\n`);
120
- if (options.mode) {
121
- write(`data: mode ${options.mode}\n`);
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 (options.selector) {
124
- write(`data: selector ${options.selector}\n`);
181
+ if (opts?.mode) {
182
+ dataLines.push(`mode ${opts.mode}`);
125
183
  }
126
- // Escape newlines in HTML for SSE format
127
- const escapedHtml = html.replace(/\n/g, "\ndata: ");
128
- write(`data: elements ${escapedHtml}\n\n`);
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
- async executeScript(script) {
132
- write(`event: datastar-execute-script\n`);
133
- write(`data: script ${script}\n\n`);
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
- try {
138
- await handler(sseStream);
139
- } finally {
140
- controller.close();
141
- }
221
+ await handler(stream);
222
+ controller.close();
142
223
  },
143
224
  });
144
225
 
145
- return new Response(stream, {
146
- headers: {
147
- "Content-Type": "text/event-stream",
148
- "Cache-Control": "no-cache",
149
- Connection: "keep-alive",
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
  }
@@ -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({ headers: c.req.raw.headers });
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({ headers: c.req.raw.headers });
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);
@@ -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
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Array length check guarantees element exists
36
- nextCursor: posts.length === limit ? sqid.encode(posts[posts.length - 1]!.id) : null,
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 ? sqid.decode(body.replyToId) ?? undefined : undefined,
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
 
@@ -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: { id: string; r2Key: string; mimeType: string; originalName: string; alt: string | null; size: number },
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 href="/dash/media/${media.id}" class="block mt-2 text-xs truncate hover:underline" title="${media.originalName}">
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 class="w-full h-full flex items-center justify-center text-muted-foreground">
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 href="/dash/media/${media.id}" class="block mt-2 text-xs truncate hover:underline" title="${media.originalName}">
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: { req: { header: (name: string) => string | undefined } }): boolean {
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({ _uploadError: "R2 storage not configured" });
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 = ["image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"];
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({ _uploadError: "File too large (max 10MB)" });
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) => {