@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.
Files changed (146) hide show
  1. package/bin/jant.js +3 -1
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +112 -85
  4. package/dist/auth.d.ts +1 -0
  5. package/dist/auth.d.ts.map +1 -1
  6. package/dist/auth.js +2 -1
  7. package/dist/client.js +1 -1
  8. package/dist/db/schema.d.ts.map +1 -1
  9. package/dist/i18n/context.d.ts.map +1 -1
  10. package/dist/i18n/context.js +0 -3
  11. package/dist/i18n/detect.d.ts +0 -11
  12. package/dist/i18n/detect.d.ts.map +1 -1
  13. package/dist/i18n/detect.js +1 -52
  14. package/dist/i18n/i18n.d.ts +4 -14
  15. package/dist/i18n/i18n.d.ts.map +1 -1
  16. package/dist/i18n/i18n.js +19 -25
  17. package/dist/i18n/index.d.ts +1 -1
  18. package/dist/i18n/index.d.ts.map +1 -1
  19. package/dist/i18n/index.js +1 -1
  20. package/dist/i18n/middleware.d.ts +2 -5
  21. package/dist/i18n/middleware.d.ts.map +1 -1
  22. package/dist/i18n/middleware.js +12 -23
  23. package/dist/lib/constants.d.ts.map +1 -1
  24. package/dist/lib/image.d.ts.map +1 -1
  25. package/dist/lib/schemas.d.ts.map +1 -1
  26. package/dist/lib/sse.d.ts +45 -17
  27. package/dist/lib/sse.d.ts.map +1 -1
  28. package/dist/lib/sse.js +77 -37
  29. package/dist/middleware/auth.d.ts.map +1 -1
  30. package/dist/routes/api/posts.js +0 -1
  31. package/dist/routes/api/upload.js +3 -1
  32. package/dist/routes/dash/collections.d.ts.map +1 -1
  33. package/dist/routes/dash/collections.js +134 -142
  34. package/dist/routes/dash/index.js +25 -26
  35. package/dist/routes/dash/media.d.ts.map +1 -1
  36. package/dist/routes/dash/media.js +60 -56
  37. package/dist/routes/dash/pages.js +64 -66
  38. package/dist/routes/dash/posts.d.ts.map +1 -1
  39. package/dist/routes/dash/posts.js +50 -59
  40. package/dist/routes/dash/redirects.d.ts.map +1 -1
  41. package/dist/routes/dash/redirects.js +63 -60
  42. package/dist/routes/dash/settings.d.ts.map +1 -1
  43. package/dist/routes/dash/settings.js +249 -93
  44. package/dist/routes/feed/rss.js +6 -4
  45. package/dist/routes/pages/archive.js +60 -62
  46. package/dist/routes/pages/collection.js +8 -8
  47. package/dist/routes/pages/home.js +14 -14
  48. package/dist/routes/pages/page.js +7 -6
  49. package/dist/routes/pages/post.js +8 -8
  50. package/dist/routes/pages/search.js +25 -27
  51. package/dist/services/collection.d.ts.map +1 -1
  52. package/dist/services/index.d.ts.map +1 -1
  53. package/dist/services/media.d.ts.map +1 -1
  54. package/dist/services/post.d.ts.map +1 -1
  55. package/dist/services/redirect.d.ts.map +1 -1
  56. package/dist/services/settings.d.ts.map +1 -1
  57. package/dist/theme/components/ActionButtons.d.ts +1 -1
  58. package/dist/theme/components/ActionButtons.d.ts.map +1 -1
  59. package/dist/theme/components/ActionButtons.js +17 -21
  60. package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
  61. package/dist/theme/components/DangerZone.d.ts.map +1 -1
  62. package/dist/theme/components/DangerZone.js +12 -15
  63. package/dist/theme/components/EmptyState.d.ts.map +1 -1
  64. package/dist/theme/components/PageForm.d.ts.map +1 -1
  65. package/dist/theme/components/PageForm.js +58 -56
  66. package/dist/theme/components/Pagination.d.ts.map +1 -1
  67. package/dist/theme/components/Pagination.js +22 -25
  68. package/dist/theme/components/PostForm.d.ts +0 -1
  69. package/dist/theme/components/PostForm.d.ts.map +1 -1
  70. package/dist/theme/components/PostForm.js +85 -77
  71. package/dist/theme/components/PostList.d.ts.map +1 -1
  72. package/dist/theme/components/PostList.js +17 -17
  73. package/dist/theme/components/ThreadView.d.ts.map +1 -1
  74. package/dist/theme/components/ThreadView.js +15 -18
  75. package/dist/theme/components/TypeBadge.d.ts.map +1 -1
  76. package/dist/theme/components/TypeBadge.js +20 -20
  77. package/dist/theme/components/VisibilityBadge.d.ts.map +1 -1
  78. package/dist/theme/components/VisibilityBadge.js +14 -14
  79. package/dist/theme/components/index.d.ts +1 -1
  80. package/dist/theme/components/index.d.ts.map +1 -1
  81. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
  82. package/dist/theme/layouts/BaseLayout.js +4 -2
  83. package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
  84. package/dist/theme/layouts/DashLayout.js +29 -29
  85. package/dist/types/lingui-react-macro.d.js +9 -0
  86. package/dist/types.d.ts +2 -0
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/vendor/datastar.js +1606 -0
  89. package/package.json +5 -2
  90. package/src/app.tsx +175 -56
  91. package/src/auth.ts +5 -1
  92. package/src/client.ts +1 -1
  93. package/src/db/schema.ts +22 -7
  94. package/src/i18n/EXAMPLES.md +34 -14
  95. package/src/i18n/README.md +19 -9
  96. package/src/i18n/context.tsx +1 -4
  97. package/src/i18n/detect.ts +1 -67
  98. package/src/i18n/i18n.ts +15 -19
  99. package/src/i18n/index.ts +0 -3
  100. package/src/i18n/middleware.ts +12 -24
  101. package/src/lib/constants.ts +2 -1
  102. package/src/lib/image-processor.ts +23 -7
  103. package/src/lib/image.ts +6 -2
  104. package/src/lib/schemas.ts +6 -2
  105. package/src/lib/sse.ts +138 -50
  106. package/src/middleware/auth.ts +6 -2
  107. package/src/routes/api/posts.ts +14 -5
  108. package/src/routes/api/upload.ts +25 -7
  109. package/src/routes/dash/collections.tsx +162 -70
  110. package/src/routes/dash/index.tsx +22 -7
  111. package/src/routes/dash/media.tsx +59 -16
  112. package/src/routes/dash/pages.tsx +102 -44
  113. package/src/routes/dash/posts.tsx +87 -54
  114. package/src/routes/dash/redirects.tsx +74 -26
  115. package/src/routes/dash/settings.tsx +250 -57
  116. package/src/routes/feed/rss.ts +6 -4
  117. package/src/routes/pages/archive.tsx +71 -21
  118. package/src/routes/pages/collection.tsx +21 -6
  119. package/src/routes/pages/home.tsx +30 -9
  120. package/src/routes/pages/page.tsx +14 -5
  121. package/src/routes/pages/post.tsx +21 -7
  122. package/src/routes/pages/search.tsx +42 -11
  123. package/src/services/collection.ts +34 -9
  124. package/src/services/index.ts +4 -1
  125. package/src/services/media.ts +15 -3
  126. package/src/services/post.ts +39 -10
  127. package/src/services/redirect.ts +4 -1
  128. package/src/services/settings.ts +14 -3
  129. package/src/theme/components/ActionButtons.tsx +26 -14
  130. package/src/theme/components/CrudPageHeader.tsx +6 -1
  131. package/src/theme/components/DangerZone.tsx +19 -13
  132. package/src/theme/components/EmptyState.tsx +6 -1
  133. package/src/theme/components/PageForm.tsx +71 -24
  134. package/src/theme/components/Pagination.tsx +26 -8
  135. package/src/theme/components/PostForm.tsx +72 -25
  136. package/src/theme/components/PostList.tsx +16 -5
  137. package/src/theme/components/ThreadView.tsx +25 -7
  138. package/src/theme/components/TypeBadge.tsx +13 -4
  139. package/src/theme/components/VisibilityBadge.tsx +17 -5
  140. package/src/theme/components/index.ts +4 -1
  141. package/src/theme/layouts/BaseLayout.tsx +5 -2
  142. package/src/theme/layouts/DashLayout.tsx +41 -12
  143. package/src/types/lingui-react-macro.d.ts +34 -0
  144. package/src/types.ts +16 -2
  145. package/src/vendor/datastar.js +9 -0
  146. package/src/vendor/datastar.js.map +7 -0
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Hono } from "hono";
8
- import { useLingui } from "../../i18n/index.js";
8
+ import { useLingui } from "@lingui/react/macro";
9
9
  import type { Bindings, Post } from "../../types.js";
10
10
  import type { AppVariables } from "../../app.js";
11
11
  import { DashLayout } from "../../theme/layouts/index.js";
@@ -20,7 +20,7 @@ import {
20
20
  } from "../../theme/components/index.js";
21
21
  import * as sqid from "../../lib/sqid.js";
22
22
  import * as time from "../../lib/time.js";
23
- import { VisibilitySchema, parseFormData } from "../../lib/schemas.js";
23
+ import { sse } from "../../lib/sse.js";
24
24
 
25
25
  type Env = { Bindings: Bindings; Variables: AppVariables };
26
26
 
@@ -33,7 +33,10 @@ function PagesListContent({ pages }: { pages: Post[] }) {
33
33
  <>
34
34
  <CrudPageHeader
35
35
  title={t({ message: "Pages", comment: "@context: Pages main heading" })}
36
- ctaLabel={t({ message: "New Page", comment: "@context: Button to create new page" })}
36
+ ctaLabel={t({
37
+ message: "New Page",
38
+ comment: "@context: Button to create new page",
39
+ })}
37
40
  ctaHref="/dash/pages/new"
38
41
  />
39
42
 
@@ -57,8 +60,15 @@ function PagesListContent({ pages }: { pages: Post[] }) {
57
60
  actions={
58
61
  <ActionButtons
59
62
  editHref={`/dash/pages/${sqid.encode(page.id)}/edit`}
60
- editLabel={t({ message: "Edit", comment: "@context: Button to edit page" })}
61
- viewHref={page.visibility !== "draft" && page.path ? `/${page.path}` : undefined}
63
+ editLabel={t({
64
+ message: "Edit",
65
+ comment: "@context: Button to edit page",
66
+ })}
67
+ viewHref={
68
+ page.visibility !== "draft" && page.path
69
+ ? `/${page.path}`
70
+ : undefined
71
+ }
62
72
  viewLabel={t({
63
73
  message: "View",
64
74
  comment: "@context: Button to view page on public site",
@@ -68,11 +78,19 @@ function PagesListContent({ pages }: { pages: Post[] }) {
68
78
  >
69
79
  <div class="flex items-center gap-2 mb-1">
70
80
  <VisibilityBadge visibility={page.visibility} />
71
- <span class="text-xs text-muted-foreground">{time.formatDate(page.updatedAt)}</span>
81
+ <span class="text-xs text-muted-foreground">
82
+ {time.formatDate(page.updatedAt)}
83
+ </span>
72
84
  </div>
73
- <a href={`/dash/pages/${sqid.encode(page.id)}`} class="font-medium hover:underline">
85
+ <a
86
+ href={`/dash/pages/${sqid.encode(page.id)}`}
87
+ class="font-medium hover:underline"
88
+ >
74
89
  {page.title ||
75
- t({ message: "Untitled", comment: "@context: Default title for untitled page" })}
90
+ t({
91
+ message: "Untitled",
92
+ comment: "@context: Default title for untitled page",
93
+ })}
76
94
  </a>
77
95
  <p class="text-sm text-muted-foreground mt-1">/{page.path}</p>
78
96
  </ListItemRow>
@@ -103,14 +121,24 @@ function ViewPageContent({ page }: { page: Post }) {
103
121
  <div>
104
122
  <h1 class="text-2xl font-semibold">
105
123
  {page.title ||
106
- t({ message: "Page", comment: "@context: Default page heading when untitled" })}
124
+ t({
125
+ message: "Page",
126
+ comment: "@context: Default page heading when untitled",
127
+ })}
107
128
  </h1>
108
129
  {page.path && <p class="text-muted-foreground mt-1">/{page.path}</p>}
109
130
  </div>
110
131
  <ActionButtons
111
132
  editHref={`/dash/pages/${sqid.encode(page.id)}/edit`}
112
- editLabel={t({ message: "Edit", comment: "@context: Button to edit page" })}
113
- viewHref={page.visibility !== "draft" && page.path ? `/${page.path}` : undefined}
133
+ editLabel={t({
134
+ message: "Edit",
135
+ comment: "@context: Button to edit page",
136
+ })}
137
+ viewHref={
138
+ page.visibility !== "draft" && page.path
139
+ ? `/${page.path}`
140
+ : undefined
141
+ }
114
142
  viewLabel={t({
115
143
  message: "View",
116
144
  comment: "@context: Button to view page on public site",
@@ -120,12 +148,18 @@ function ViewPageContent({ page }: { page: Post }) {
120
148
 
121
149
  <div class="card">
122
150
  <section>
123
- <div class="prose" dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }} />
151
+ <div
152
+ class="prose"
153
+ dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }}
154
+ />
124
155
  </section>
125
156
  </div>
126
157
 
127
158
  <DangerZone
128
- actionLabel={t({ message: "Delete Page", comment: "@context: Button to delete page" })}
159
+ actionLabel={t({
160
+ message: "Delete Page",
161
+ comment: "@context: Button to delete page",
162
+ })}
129
163
  formAction={`/dash/pages/${sqid.encode(page.id)}/delete`}
130
164
  confirmMessage="Are you sure you want to delete this page?"
131
165
  />
@@ -138,7 +172,10 @@ function EditPageContent({ page }: { page: Post }) {
138
172
  return (
139
173
  <>
140
174
  <h1 class="text-2xl font-semibold mb-6">
141
- {t({ message: "Edit Page", comment: "@context: Edit page main heading" })}
175
+ {t({
176
+ message: "Edit Page",
177
+ comment: "@context: Edit page main heading",
178
+ })}
142
179
  </h1>
143
180
  <PageForm page={page} action={`/dash/pages/${sqid.encode(page.id)}`} />
144
181
  </>
@@ -155,9 +192,14 @@ pagesRoutes.get("/", async (c) => {
155
192
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
156
193
 
157
194
  return c.html(
158
- <DashLayout c={c} title="Pages" siteName={siteName} currentPath="/dash/pages">
195
+ <DashLayout
196
+ c={c}
197
+ title="Pages"
198
+ siteName={siteName}
199
+ currentPath="/dash/pages"
200
+ >
159
201
  <PagesListContent pages={pages} />
160
- </DashLayout>
202
+ </DashLayout>,
161
203
  );
162
204
  });
163
205
 
@@ -166,30 +208,37 @@ pagesRoutes.get("/new", async (c) => {
166
208
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
167
209
 
168
210
  return c.html(
169
- <DashLayout c={c} title="New Page" siteName={siteName} currentPath="/dash/pages">
211
+ <DashLayout
212
+ c={c}
213
+ title="New Page"
214
+ siteName={siteName}
215
+ currentPath="/dash/pages"
216
+ >
170
217
  <NewPageContent />
171
- </DashLayout>
218
+ </DashLayout>,
172
219
  );
173
220
  });
174
221
 
175
222
  // Create page
176
223
  pagesRoutes.post("/", async (c) => {
177
- const formData = await c.req.formData();
178
-
179
- const title = formData.get("title") as string;
180
- const content = formData.get("content") as string;
181
- const visibility = parseFormData(formData, "visibility", VisibilitySchema);
182
- const path = formData.get("path") as string;
224
+ const body = await c.req.json<{
225
+ title: string;
226
+ content: string;
227
+ visibility: string;
228
+ path: string;
229
+ }>();
183
230
 
184
231
  const page = await c.var.services.posts.create({
185
232
  type: "page",
186
- title,
187
- content,
188
- visibility,
189
- path: path.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
233
+ title: body.title,
234
+ content: body.content,
235
+ visibility: body.visibility as Post["visibility"],
236
+ path: body.path.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
190
237
  });
191
238
 
192
- return c.redirect(`/dash/pages/${sqid.encode(page.id)}`);
239
+ return sse(c, async (stream) => {
240
+ await stream.redirect(`/dash/pages/${sqid.encode(page.id)}`);
241
+ });
193
242
  });
194
243
 
195
244
  // View single page
@@ -203,9 +252,14 @@ pagesRoutes.get("/:id", async (c) => {
203
252
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
204
253
 
205
254
  return c.html(
206
- <DashLayout c={c} title={page.title || "Page"} siteName={siteName} currentPath="/dash/pages">
255
+ <DashLayout
256
+ c={c}
257
+ title={page.title || "Page"}
258
+ siteName={siteName}
259
+ currentPath="/dash/pages"
260
+ >
207
261
  <ViewPageContent page={page} />
208
- </DashLayout>
262
+ </DashLayout>,
209
263
  );
210
264
  });
211
265
 
@@ -227,7 +281,7 @@ pagesRoutes.get("/:id/edit", async (c) => {
227
281
  currentPath="/dash/pages"
228
282
  >
229
283
  <EditPageContent page={page} />
230
- </DashLayout>
284
+ </DashLayout>,
231
285
  );
232
286
  });
233
287
 
@@ -236,22 +290,24 @@ pagesRoutes.post("/:id", async (c) => {
236
290
  const id = sqid.decode(c.req.param("id"));
237
291
  if (!id) return c.notFound();
238
292
 
239
- const formData = await c.req.formData();
240
-
241
- const title = formData.get("title") as string;
242
- const content = formData.get("content") as string;
243
- const visibility = parseFormData(formData, "visibility", VisibilitySchema);
244
- const path = formData.get("path") as string;
293
+ const body = await c.req.json<{
294
+ title: string;
295
+ content: string;
296
+ visibility: string;
297
+ path: string;
298
+ }>();
245
299
 
246
300
  await c.var.services.posts.update(id, {
247
301
  type: "page",
248
- title,
249
- content,
250
- visibility,
251
- path: path.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
302
+ title: body.title,
303
+ content: body.content,
304
+ visibility: body.visibility as Post["visibility"],
305
+ path: body.path.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
252
306
  });
253
307
 
254
- return c.redirect(`/dash/pages/${sqid.encode(id)}`);
308
+ return sse(c, async (stream) => {
309
+ await stream.redirect(`/dash/pages/${sqid.encode(id)}`);
310
+ });
255
311
  });
256
312
 
257
313
  // Delete page
@@ -261,5 +317,7 @@ pagesRoutes.post("/:id/delete", async (c) => {
261
317
 
262
318
  await c.var.services.posts.delete(id);
263
319
 
264
- return c.redirect("/dash/pages");
320
+ return sse(c, async (stream) => {
321
+ await stream.redirect("/dash/pages");
322
+ });
265
323
  });
@@ -3,19 +3,18 @@
3
3
  */
4
4
 
5
5
  import { Hono } from "hono";
6
- import { z } from "zod";
7
- import { useLingui } from "../../i18n/index.js";
6
+ import { useLingui } from "@lingui/react/macro";
8
7
  import type { Bindings, Post } from "../../types.js";
9
8
  import type { AppVariables } from "../../app.js";
10
9
  import { DashLayout } from "../../theme/layouts/index.js";
11
- import { PostForm, PostList, CrudPageHeader, ActionButtons } from "../../theme/components/index.js";
12
- import * as sqid from "../../lib/sqid.js";
13
10
  import {
14
- PostTypeSchema,
15
- VisibilitySchema,
16
- parseFormData,
17
- parseFormDataOptional,
18
- } from "../../lib/schemas.js";
11
+ PostForm,
12
+ PostList,
13
+ CrudPageHeader,
14
+ ActionButtons,
15
+ } from "../../theme/components/index.js";
16
+ import * as sqid from "../../lib/sqid.js";
17
+ import { sse } from "../../lib/sse.js";
19
18
 
20
19
  type Env = { Bindings: Bindings; Variables: AppVariables };
21
20
 
@@ -27,7 +26,10 @@ function PostsListContent({ posts }: { posts: Post[] }) {
27
26
  <>
28
27
  <CrudPageHeader
29
28
  title={t({ message: "Posts", comment: "@context: Dashboard heading" })}
30
- ctaLabel={t({ message: "New Post", comment: "@context: Button to create new post" })}
29
+ ctaLabel={t({
30
+ message: "New Post",
31
+ comment: "@context: Button to create new post",
32
+ })}
31
33
  ctaHref="/dash/posts/new"
32
34
  />
33
35
  <PostList posts={posts} />
@@ -55,9 +57,14 @@ postsRoutes.get("/", async (c) => {
55
57
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
56
58
 
57
59
  return c.html(
58
- <DashLayout c={c} title="Posts" siteName={siteName} currentPath="/dash/posts">
60
+ <DashLayout
61
+ c={c}
62
+ title="Posts"
63
+ siteName={siteName}
64
+ currentPath="/dash/posts"
65
+ >
59
66
  <PostsListContent posts={posts} />
60
- </DashLayout>
67
+ </DashLayout>,
61
68
  );
62
69
  });
63
70
 
@@ -66,39 +73,48 @@ postsRoutes.get("/new", async (c) => {
66
73
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
67
74
 
68
75
  return c.html(
69
- <DashLayout c={c} title="New Post" siteName={siteName} currentPath="/dash/posts">
76
+ <DashLayout
77
+ c={c}
78
+ title="New Post"
79
+ siteName={siteName}
80
+ currentPath="/dash/posts"
81
+ >
70
82
  <NewPostContent />
71
- </DashLayout>
83
+ </DashLayout>,
72
84
  );
73
85
  });
74
86
 
75
87
  // Create post
76
88
  postsRoutes.post("/", async (c) => {
77
- const formData = await c.req.formData();
78
-
79
- // Validate and parse form data
80
- const type = parseFormData(formData, "type", PostTypeSchema);
81
- const visibility = parseFormData(formData, "visibility", VisibilitySchema);
82
- const title = parseFormDataOptional(formData, "title", z.string());
83
- const content = formData.get("content") as string;
84
- const sourceUrl = parseFormDataOptional(formData, "sourceUrl", z.string());
85
- const path = parseFormDataOptional(formData, "path", z.string());
89
+ const body = await c.req.json<{
90
+ type: string;
91
+ title?: string;
92
+ content: string;
93
+ visibility: string;
94
+ sourceUrl?: string;
95
+ path?: string;
96
+ }>();
86
97
 
87
98
  const post = await c.var.services.posts.create({
88
- type,
89
- title,
90
- content,
91
- visibility,
92
- sourceUrl,
93
- path,
99
+ type: body.type as Post["type"],
100
+ title: body.title || undefined,
101
+ content: body.content,
102
+ visibility: body.visibility as Post["visibility"],
103
+ sourceUrl: body.sourceUrl || undefined,
104
+ path: body.path || undefined,
94
105
  });
95
106
 
96
- return c.redirect(`/dash/posts/${sqid.encode(post.id)}`);
107
+ return sse(c, async (stream) => {
108
+ await stream.redirect(`/dash/posts/${sqid.encode(post.id)}`);
109
+ });
97
110
  });
98
111
 
99
112
  function ViewPostContent({ post }: { post: Post }) {
100
113
  const { t } = useLingui();
101
- const defaultTitle = t({ message: "Post", comment: "@context: Default post title" });
114
+ const defaultTitle = t({
115
+ message: "Post",
116
+ comment: "@context: Default post title",
117
+ });
102
118
 
103
119
  return (
104
120
  <>
@@ -106,15 +122,24 @@ function ViewPostContent({ post }: { post: Post }) {
106
122
  <h1 class="text-2xl font-semibold">{post.title || defaultTitle}</h1>
107
123
  <ActionButtons
108
124
  editHref={`/dash/posts/${sqid.encode(post.id)}/edit`}
109
- editLabel={t({ message: "Edit", comment: "@context: Button to edit post" })}
125
+ editLabel={t({
126
+ message: "Edit",
127
+ comment: "@context: Button to edit post",
128
+ })}
110
129
  viewHref={`/p/${sqid.encode(post.id)}`}
111
- viewLabel={t({ message: "View", comment: "@context: Button to view post" })}
130
+ viewLabel={t({
131
+ message: "View",
132
+ comment: "@context: Button to view post",
133
+ })}
112
134
  />
113
135
  </div>
114
136
 
115
137
  <div class="card">
116
138
  <section>
117
- <div class="prose" dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }} />
139
+ <div
140
+ class="prose"
141
+ dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
142
+ />
118
143
  </section>
119
144
  </div>
120
145
  </>
@@ -145,9 +170,14 @@ postsRoutes.get("/:id", async (c) => {
145
170
  const pageTitle = post.title || "Post";
146
171
 
147
172
  return c.html(
148
- <DashLayout c={c} title={pageTitle} siteName={siteName} currentPath="/dash/posts">
173
+ <DashLayout
174
+ c={c}
175
+ title={pageTitle}
176
+ siteName={siteName}
177
+ currentPath="/dash/posts"
178
+ >
149
179
  <ViewPostContent post={post} />
150
- </DashLayout>
180
+ </DashLayout>,
151
181
  );
152
182
  });
153
183
 
@@ -169,7 +199,7 @@ postsRoutes.get("/:id/edit", async (c) => {
169
199
  currentPath="/dash/posts"
170
200
  >
171
201
  <EditPostContent post={post} />
172
- </DashLayout>
202
+ </DashLayout>,
173
203
  );
174
204
  });
175
205
 
@@ -178,26 +208,27 @@ postsRoutes.post("/:id", async (c) => {
178
208
  const id = sqid.decode(c.req.param("id"));
179
209
  if (!id) return c.notFound();
180
210
 
181
- const formData = await c.req.formData();
182
-
183
- // Validate and parse form data
184
- const type = parseFormData(formData, "type", PostTypeSchema);
185
- const visibility = parseFormData(formData, "visibility", VisibilitySchema);
186
- const title = parseFormDataOptional(formData, "title", z.string()) || null;
187
- const content = parseFormDataOptional(formData, "content", z.string()) || null;
188
- const sourceUrl = parseFormDataOptional(formData, "sourceUrl", z.string()) || null;
189
- const path = parseFormDataOptional(formData, "path", z.string()) || null;
211
+ const body = await c.req.json<{
212
+ type: string;
213
+ title?: string;
214
+ content?: string;
215
+ visibility: string;
216
+ sourceUrl?: string;
217
+ path?: string;
218
+ }>();
190
219
 
191
220
  await c.var.services.posts.update(id, {
192
- type,
193
- title,
194
- content,
195
- visibility,
196
- sourceUrl,
197
- path,
221
+ type: body.type as Post["type"],
222
+ title: body.title || null,
223
+ content: body.content || null,
224
+ visibility: body.visibility as Post["visibility"],
225
+ sourceUrl: body.sourceUrl || null,
226
+ path: body.path || null,
198
227
  });
199
228
 
200
- return c.redirect(`/dash/posts/${sqid.encode(id)}`);
229
+ return sse(c, async (stream) => {
230
+ await stream.redirect(`/dash/posts/${sqid.encode(id)}`);
231
+ });
201
232
  });
202
233
 
203
234
  // Delete post
@@ -207,5 +238,7 @@ postsRoutes.post("/:id/delete", async (c) => {
207
238
 
208
239
  await c.var.services.posts.delete(id);
209
240
 
210
- return c.redirect("/dash/posts");
241
+ return sse(c, async (stream) => {
242
+ await stream.redirect("/dash/posts");
243
+ });
211
244
  });
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { Hono } from "hono";
6
- import { useLingui } from "../../i18n/index.js";
6
+ import { useLingui } from "@lingui/react/macro";
7
7
  import type { Bindings, Redirect } from "../../types.js";
8
8
  import type { AppVariables } from "../../app.js";
9
9
  import { DashLayout } from "../../theme/layouts/index.js";
@@ -13,6 +13,7 @@ import {
13
13
  ActionButtons,
14
14
  CrudPageHeader,
15
15
  } from "../../theme/components/index.js";
16
+ import { sse } from "../../lib/sse.js";
16
17
 
17
18
  type Env = { Bindings: Bindings; Variables: AppVariables };
18
19
 
@@ -24,7 +25,10 @@ function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
24
25
  return (
25
26
  <>
26
27
  <CrudPageHeader
27
- title={t({ message: "Redirects", comment: "@context: Dashboard heading" })}
28
+ title={t({
29
+ message: "Redirects",
30
+ comment: "@context: Dashboard heading",
31
+ })}
28
32
  ctaLabel={t({
29
33
  message: "New Redirect",
30
34
  comment: "@context: Button to create new redirect",
@@ -82,12 +86,25 @@ function NewRedirectContent() {
82
86
  {t({ message: "New Redirect", comment: "@context: Page heading" })}
83
87
  </h1>
84
88
 
85
- <form method="post" action="/dash/redirects" class="flex flex-col gap-4 max-w-lg">
89
+ <form
90
+ data-signals="{fromPath: '', toPath: '', type: '301'}"
91
+ data-on:submit__prevent="@post('/dash/redirects')"
92
+ class="flex flex-col gap-4 max-w-lg"
93
+ >
86
94
  <div class="field">
87
95
  <label class="label">
88
- {t({ message: "From Path", comment: "@context: Redirect form field" })}
96
+ {t({
97
+ message: "From Path",
98
+ comment: "@context: Redirect form field",
99
+ })}
89
100
  </label>
90
- <input type="text" name="fromPath" class="input" placeholder="/old-path" required />
101
+ <input
102
+ type="text"
103
+ data-bind="fromPath"
104
+ class="input"
105
+ placeholder="/old-path"
106
+ required
107
+ />
91
108
  <p class="text-xs text-muted-foreground mt-1">
92
109
  {t({
93
110
  message: "The path to redirect from",
@@ -98,11 +115,14 @@ function NewRedirectContent() {
98
115
 
99
116
  <div class="field">
100
117
  <label class="label">
101
- {t({ message: "To Path", comment: "@context: Redirect form field" })}
118
+ {t({
119
+ message: "To Path",
120
+ comment: "@context: Redirect form field",
121
+ })}
102
122
  </label>
103
123
  <input
104
124
  type="text"
105
- name="toPath"
125
+ data-bind="toPath"
106
126
  class="input"
107
127
  placeholder="/new-path or https://..."
108
128
  required
@@ -119,22 +139,34 @@ function NewRedirectContent() {
119
139
  <label class="label">
120
140
  {t({ message: "Type", comment: "@context: Redirect form field" })}
121
141
  </label>
122
- <select name="type" class="select">
142
+ <select data-bind="type" class="select">
123
143
  <option value="301">
124
- {t({ message: "301 (Permanent)", comment: "@context: Redirect type option" })}
144
+ {t({
145
+ message: "301 (Permanent)",
146
+ comment: "@context: Redirect type option",
147
+ })}
125
148
  </option>
126
149
  <option value="302">
127
- {t({ message: "302 (Temporary)", comment: "@context: Redirect type option" })}
150
+ {t({
151
+ message: "302 (Temporary)",
152
+ comment: "@context: Redirect type option",
153
+ })}
128
154
  </option>
129
155
  </select>
130
156
  </div>
131
157
 
132
158
  <div class="flex gap-2">
133
159
  <button type="submit" class="btn">
134
- {t({ message: "Create Redirect", comment: "@context: Button to save new redirect" })}
160
+ {t({
161
+ message: "Create Redirect",
162
+ comment: "@context: Button to save new redirect",
163
+ })}
135
164
  </button>
136
165
  <a href="/dash/redirects" class="btn-outline">
137
- {t({ message: "Cancel", comment: "@context: Button to cancel form" })}
166
+ {t({
167
+ message: "Cancel",
168
+ comment: "@context: Button to cancel form",
169
+ })}
138
170
  </a>
139
171
  </div>
140
172
  </form>
@@ -148,9 +180,14 @@ redirectsRoutes.get("/", async (c) => {
148
180
  const redirects = await c.var.services.redirects.list();
149
181
 
150
182
  return c.html(
151
- <DashLayout c={c} title="Redirects" siteName={siteName} currentPath="/dash/redirects">
183
+ <DashLayout
184
+ c={c}
185
+ title="Redirects"
186
+ siteName={siteName}
187
+ currentPath="/dash/redirects"
188
+ >
152
189
  <RedirectsListContent redirects={redirects} />
153
- </DashLayout>
190
+ </DashLayout>,
154
191
  );
155
192
  });
156
193
 
@@ -159,23 +196,31 @@ redirectsRoutes.get("/new", async (c) => {
159
196
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
160
197
 
161
198
  return c.html(
162
- <DashLayout c={c} title="New Redirect" siteName={siteName} currentPath="/dash/redirects">
199
+ <DashLayout
200
+ c={c}
201
+ title="New Redirect"
202
+ siteName={siteName}
203
+ currentPath="/dash/redirects"
204
+ >
163
205
  <NewRedirectContent />
164
- </DashLayout>
206
+ </DashLayout>,
165
207
  );
166
208
  });
167
209
 
168
210
  // Create redirect
169
211
  redirectsRoutes.post("/", async (c) => {
170
- const formData = await c.req.formData();
171
-
172
- const fromPath = formData.get("fromPath") as string;
173
- const toPath = formData.get("toPath") as string;
174
- const type = parseInt(formData.get("type") as string, 10) as 301 | 302;
175
-
176
- await c.var.services.redirects.create(fromPath, toPath, type);
177
-
178
- return c.redirect("/dash/redirects");
212
+ const body = await c.req.json<{
213
+ fromPath: string;
214
+ toPath: string;
215
+ type: string;
216
+ }>();
217
+
218
+ const type = parseInt(body.type, 10) as 301 | 302;
219
+ await c.var.services.redirects.create(body.fromPath, body.toPath, type);
220
+
221
+ return sse(c, async (stream) => {
222
+ await stream.redirect("/dash/redirects");
223
+ });
179
224
  });
180
225
 
181
226
  // Delete redirect
@@ -184,5 +229,8 @@ redirectsRoutes.post("/:id/delete", async (c) => {
184
229
  if (!isNaN(id)) {
185
230
  await c.var.services.redirects.delete(id);
186
231
  }
187
- return c.redirect("/dash/redirects");
232
+
233
+ return sse(c, async (stream) => {
234
+ await stream.redirect("/dash/redirects");
235
+ });
188
236
  });