@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
@@ -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} />
@@ -39,7 +41,9 @@ function NewPostContent() {
39
41
  const { t } = useLingui();
40
42
  return (
41
43
  <>
42
- <h1 class="text-2xl font-semibold mb-6">{t({ message: "New Post", comment: "@context: Page heading" })}</h1>
44
+ <h1 class="text-2xl font-semibold mb-6">
45
+ {t({ message: "New Post", comment: "@context: Page heading" })}
46
+ </h1>
43
47
  <PostForm action="/dash/posts" />
44
48
  </>
45
49
  );
@@ -53,9 +57,14 @@ postsRoutes.get("/", async (c) => {
53
57
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
54
58
 
55
59
  return c.html(
56
- <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
+ >
57
66
  <PostsListContent posts={posts} />
58
- </DashLayout>
67
+ </DashLayout>,
59
68
  );
60
69
  });
61
70
 
@@ -64,39 +73,48 @@ postsRoutes.get("/new", async (c) => {
64
73
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
65
74
 
66
75
  return c.html(
67
- <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
+ >
68
82
  <NewPostContent />
69
- </DashLayout>
83
+ </DashLayout>,
70
84
  );
71
85
  });
72
86
 
73
87
  // Create post
74
88
  postsRoutes.post("/", async (c) => {
75
- const formData = await c.req.formData();
76
-
77
- // Validate and parse form data
78
- const type = parseFormData(formData, "type", PostTypeSchema);
79
- const visibility = parseFormData(formData, "visibility", VisibilitySchema);
80
- const title = parseFormDataOptional(formData, "title", z.string());
81
- const content = formData.get("content") as string;
82
- const sourceUrl = parseFormDataOptional(formData, "sourceUrl", z.string());
83
- 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
+ }>();
84
97
 
85
98
  const post = await c.var.services.posts.create({
86
- type,
87
- title,
88
- content,
89
- visibility,
90
- sourceUrl,
91
- 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,
92
105
  });
93
106
 
94
- 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
+ });
95
110
  });
96
111
 
97
112
  function ViewPostContent({ post }: { post: Post }) {
98
113
  const { t } = useLingui();
99
- const defaultTitle = t({ message: "Post", comment: "@context: Default post title" });
114
+ const defaultTitle = t({
115
+ message: "Post",
116
+ comment: "@context: Default post title",
117
+ });
100
118
 
101
119
  return (
102
120
  <>
@@ -104,15 +122,24 @@ function ViewPostContent({ post }: { post: Post }) {
104
122
  <h1 class="text-2xl font-semibold">{post.title || defaultTitle}</h1>
105
123
  <ActionButtons
106
124
  editHref={`/dash/posts/${sqid.encode(post.id)}/edit`}
107
- editLabel={t({ message: "Edit", comment: "@context: Button to edit post" })}
125
+ editLabel={t({
126
+ message: "Edit",
127
+ comment: "@context: Button to edit post",
128
+ })}
108
129
  viewHref={`/p/${sqid.encode(post.id)}`}
109
- viewLabel={t({ message: "View", comment: "@context: Button to view post" })}
130
+ viewLabel={t({
131
+ message: "View",
132
+ comment: "@context: Button to view post",
133
+ })}
110
134
  />
111
135
  </div>
112
136
 
113
137
  <div class="card">
114
138
  <section>
115
- <div class="prose" dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }} />
139
+ <div
140
+ class="prose"
141
+ dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
142
+ />
116
143
  </section>
117
144
  </div>
118
145
  </>
@@ -123,7 +150,9 @@ function EditPostContent({ post }: { post: Post }) {
123
150
  const { t } = useLingui();
124
151
  return (
125
152
  <>
126
- <h1 class="text-2xl font-semibold mb-6">{t({ message: "Edit Post", comment: "@context: Page heading" })}</h1>
153
+ <h1 class="text-2xl font-semibold mb-6">
154
+ {t({ message: "Edit Post", comment: "@context: Page heading" })}
155
+ </h1>
127
156
  <PostForm post={post} action={`/dash/posts/${sqid.encode(post.id)}`} />
128
157
  </>
129
158
  );
@@ -141,9 +170,14 @@ postsRoutes.get("/:id", async (c) => {
141
170
  const pageTitle = post.title || "Post";
142
171
 
143
172
  return c.html(
144
- <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
+ >
145
179
  <ViewPostContent post={post} />
146
- </DashLayout>
180
+ </DashLayout>,
147
181
  );
148
182
  });
149
183
 
@@ -158,9 +192,14 @@ postsRoutes.get("/:id/edit", async (c) => {
158
192
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
159
193
 
160
194
  return c.html(
161
- <DashLayout c={c} title={`Edit: ${post.title || "Post"}`} siteName={siteName} currentPath="/dash/posts">
195
+ <DashLayout
196
+ c={c}
197
+ title={`Edit: ${post.title || "Post"}`}
198
+ siteName={siteName}
199
+ currentPath="/dash/posts"
200
+ >
162
201
  <EditPostContent post={post} />
163
- </DashLayout>
202
+ </DashLayout>,
164
203
  );
165
204
  });
166
205
 
@@ -169,26 +208,27 @@ postsRoutes.post("/:id", async (c) => {
169
208
  const id = sqid.decode(c.req.param("id"));
170
209
  if (!id) return c.notFound();
171
210
 
172
- const formData = await c.req.formData();
173
-
174
- // Validate and parse form data
175
- const type = parseFormData(formData, "type", PostTypeSchema);
176
- const visibility = parseFormData(formData, "visibility", VisibilitySchema);
177
- const title = parseFormDataOptional(formData, "title", z.string()) || null;
178
- const content = parseFormDataOptional(formData, "content", z.string()) || null;
179
- const sourceUrl = parseFormDataOptional(formData, "sourceUrl", z.string()) || null;
180
- 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
+ }>();
181
219
 
182
220
  await c.var.services.posts.update(id, {
183
- type,
184
- title,
185
- content,
186
- visibility,
187
- sourceUrl,
188
- 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,
189
227
  });
190
228
 
191
- return c.redirect(`/dash/posts/${sqid.encode(id)}`);
229
+ return sse(c, async (stream) => {
230
+ await stream.redirect(`/dash/posts/${sqid.encode(id)}`);
231
+ });
192
232
  });
193
233
 
194
234
  // Delete post
@@ -198,5 +238,7 @@ postsRoutes.post("/:id/delete", async (c) => {
198
238
 
199
239
  await c.var.services.posts.delete(id);
200
240
 
201
- return c.redirect("/dash/posts");
241
+ return sse(c, async (stream) => {
242
+ await stream.redirect("/dash/posts");
243
+ });
202
244
  });
@@ -3,11 +3,17 @@
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";
10
- import { EmptyState, ListItemRow, ActionButtons, CrudPageHeader } from "../../theme/components/index.js";
10
+ import {
11
+ EmptyState,
12
+ ListItemRow,
13
+ ActionButtons,
14
+ CrudPageHeader,
15
+ } from "../../theme/components/index.js";
16
+ import { sse } from "../../lib/sse.js";
11
17
 
12
18
  type Env = { Bindings: Bindings; Variables: AppVariables };
13
19
 
@@ -19,15 +25,27 @@ function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
19
25
  return (
20
26
  <>
21
27
  <CrudPageHeader
22
- title={t({ message: "Redirects", comment: "@context: Dashboard heading" })}
23
- ctaLabel={t({ message: "New Redirect", comment: "@context: Button to create new redirect" })}
28
+ title={t({
29
+ message: "Redirects",
30
+ comment: "@context: Dashboard heading",
31
+ })}
32
+ ctaLabel={t({
33
+ message: "New Redirect",
34
+ comment: "@context: Button to create new redirect",
35
+ })}
24
36
  ctaHref="/dash/redirects/new"
25
37
  />
26
38
 
27
39
  {redirects.length === 0 ? (
28
40
  <EmptyState
29
- message={t({ message: "No redirects configured.", comment: "@context: Empty state message" })}
30
- ctaText={t({ message: "New Redirect", comment: "@context: Button to create new redirect" })}
41
+ message={t({
42
+ message: "No redirects configured.",
43
+ comment: "@context: Empty state message",
44
+ })}
45
+ ctaText={t({
46
+ message: "New Redirect",
47
+ comment: "@context: Button to create new redirect",
48
+ })}
31
49
  ctaHref="/dash/redirects/new"
32
50
  />
33
51
  ) : (
@@ -38,7 +56,10 @@ function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
38
56
  actions={
39
57
  <ActionButtons
40
58
  deleteAction={`/dash/redirects/${r.id}/delete`}
41
- deleteLabel={t({ message: "Delete", comment: "@context: Button to delete redirect" })}
59
+ deleteLabel={t({
60
+ message: "Delete",
61
+ comment: "@context: Button to delete redirect",
62
+ })}
42
63
  />
43
64
  }
44
65
  >
@@ -61,47 +82,91 @@ function NewRedirectContent() {
61
82
 
62
83
  return (
63
84
  <>
64
- <h1 class="text-2xl font-semibold mb-6">{t({ message: "New Redirect", comment: "@context: Page heading" })}</h1>
65
-
66
- <form method="post" action="/dash/redirects" class="flex flex-col gap-4 max-w-lg">
85
+ <h1 class="text-2xl font-semibold mb-6">
86
+ {t({ message: "New Redirect", comment: "@context: Page heading" })}
87
+ </h1>
88
+
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
+ >
67
94
  <div class="field">
68
- <label class="label">{t({ message: "From Path", comment: "@context: Redirect form field" })}</label>
95
+ <label class="label">
96
+ {t({
97
+ message: "From Path",
98
+ comment: "@context: Redirect form field",
99
+ })}
100
+ </label>
69
101
  <input
70
102
  type="text"
71
- name="fromPath"
103
+ data-bind="fromPath"
72
104
  class="input"
73
105
  placeholder="/old-path"
74
106
  required
75
107
  />
76
- <p class="text-xs text-muted-foreground mt-1">{t({ message: "The path to redirect from", comment: "@context: Redirect from path help text" })}</p>
108
+ <p class="text-xs text-muted-foreground mt-1">
109
+ {t({
110
+ message: "The path to redirect from",
111
+ comment: "@context: Redirect from path help text",
112
+ })}
113
+ </p>
77
114
  </div>
78
115
 
79
116
  <div class="field">
80
- <label class="label">{t({ message: "To Path", comment: "@context: Redirect form field" })}</label>
117
+ <label class="label">
118
+ {t({
119
+ message: "To Path",
120
+ comment: "@context: Redirect form field",
121
+ })}
122
+ </label>
81
123
  <input
82
124
  type="text"
83
- name="toPath"
125
+ data-bind="toPath"
84
126
  class="input"
85
127
  placeholder="/new-path or https://..."
86
128
  required
87
129
  />
88
- <p class="text-xs text-muted-foreground mt-1">{t({ message: "The destination path or URL", comment: "@context: Redirect to path help text" })}</p>
130
+ <p class="text-xs text-muted-foreground mt-1">
131
+ {t({
132
+ message: "The destination path or URL",
133
+ comment: "@context: Redirect to path help text",
134
+ })}
135
+ </p>
89
136
  </div>
90
137
 
91
138
  <div class="field">
92
- <label class="label">{t({ message: "Type", comment: "@context: Redirect form field" })}</label>
93
- <select name="type" class="select">
94
- <option value="301">{t({ message: "301 (Permanent)", comment: "@context: Redirect type option" })}</option>
95
- <option value="302">{t({ message: "302 (Temporary)", comment: "@context: Redirect type option" })}</option>
139
+ <label class="label">
140
+ {t({ message: "Type", comment: "@context: Redirect form field" })}
141
+ </label>
142
+ <select data-bind="type" class="select">
143
+ <option value="301">
144
+ {t({
145
+ message: "301 (Permanent)",
146
+ comment: "@context: Redirect type option",
147
+ })}
148
+ </option>
149
+ <option value="302">
150
+ {t({
151
+ message: "302 (Temporary)",
152
+ comment: "@context: Redirect type option",
153
+ })}
154
+ </option>
96
155
  </select>
97
156
  </div>
98
157
 
99
158
  <div class="flex gap-2">
100
159
  <button type="submit" class="btn">
101
- {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
+ })}
102
164
  </button>
103
165
  <a href="/dash/redirects" class="btn-outline">
104
- {t({ message: "Cancel", comment: "@context: Button to cancel form" })}
166
+ {t({
167
+ message: "Cancel",
168
+ comment: "@context: Button to cancel form",
169
+ })}
105
170
  </a>
106
171
  </div>
107
172
  </form>
@@ -115,9 +180,14 @@ redirectsRoutes.get("/", async (c) => {
115
180
  const redirects = await c.var.services.redirects.list();
116
181
 
117
182
  return c.html(
118
- <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
+ >
119
189
  <RedirectsListContent redirects={redirects} />
120
- </DashLayout>
190
+ </DashLayout>,
121
191
  );
122
192
  });
123
193
 
@@ -126,23 +196,31 @@ redirectsRoutes.get("/new", async (c) => {
126
196
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
127
197
 
128
198
  return c.html(
129
- <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
+ >
130
205
  <NewRedirectContent />
131
- </DashLayout>
206
+ </DashLayout>,
132
207
  );
133
208
  });
134
209
 
135
210
  // Create redirect
136
211
  redirectsRoutes.post("/", async (c) => {
137
- const formData = await c.req.formData();
138
-
139
- const fromPath = formData.get("fromPath") as string;
140
- const toPath = formData.get("toPath") as string;
141
- const type = parseInt(formData.get("type") as string, 10) as 301 | 302;
142
-
143
- await c.var.services.redirects.create(fromPath, toPath, type);
144
-
145
- 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
+ });
146
224
  });
147
225
 
148
226
  // Delete redirect
@@ -151,5 +229,8 @@ redirectsRoutes.post("/:id/delete", async (c) => {
151
229
  if (!isNaN(id)) {
152
230
  await c.var.services.redirects.delete(id);
153
231
  }
154
- return c.redirect("/dash/redirects");
232
+
233
+ return sse(c, async (stream) => {
234
+ await stream.redirect("/dash/redirects");
235
+ });
155
236
  });