@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,32 +3,55 @@
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, Collection, Post } 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, DangerZone } from "../../theme/components/index.js";
10
+ import {
11
+ EmptyState,
12
+ ListItemRow,
13
+ ActionButtons,
14
+ CrudPageHeader,
15
+ DangerZone,
16
+ } from "../../theme/components/index.js";
11
17
  import * as sqid from "../../lib/sqid.js";
18
+ import { sse } from "../../lib/sse.js";
12
19
 
13
20
  type Env = { Bindings: Bindings; Variables: AppVariables };
14
21
 
15
22
  export const collectionsRoutes = new Hono<Env>();
16
23
 
17
- function CollectionsListContent({ collections }: { collections: Collection[] }) {
24
+ function CollectionsListContent({
25
+ collections,
26
+ }: {
27
+ collections: Collection[];
28
+ }) {
18
29
  const { t } = useLingui();
19
30
 
20
31
  return (
21
32
  <>
22
33
  <CrudPageHeader
23
- title={t({ message: "Collections", comment: "@context: Dashboard heading" })}
24
- ctaLabel={t({ message: "New Collection", comment: "@context: Button to create new collection" })}
34
+ title={t({
35
+ message: "Collections",
36
+ comment: "@context: Dashboard heading",
37
+ })}
38
+ ctaLabel={t({
39
+ message: "New Collection",
40
+ comment: "@context: Button to create new collection",
41
+ })}
25
42
  ctaHref="/dash/collections/new"
26
43
  />
27
44
 
28
45
  {collections.length === 0 ? (
29
46
  <EmptyState
30
- message={t({ message: "No collections yet.", comment: "@context: Empty state message" })}
31
- ctaText={t({ message: "New Collection", comment: "@context: Button to create new collection" })}
47
+ message={t({
48
+ message: "No collections yet.",
49
+ comment: "@context: Empty state message",
50
+ })}
51
+ ctaText={t({
52
+ message: "New Collection",
53
+ comment: "@context: Button to create new collection",
54
+ })}
32
55
  ctaHref="/dash/collections/new"
33
56
  />
34
57
  ) : (
@@ -39,18 +62,29 @@ function CollectionsListContent({ collections }: { collections: Collection[] })
39
62
  actions={
40
63
  <ActionButtons
41
64
  editHref={`/dash/collections/${col.id}/edit`}
42
- editLabel={t({ message: "Edit", comment: "@context: Button to edit collection" })}
65
+ editLabel={t({
66
+ message: "Edit",
67
+ comment: "@context: Button to edit collection",
68
+ })}
43
69
  viewHref={`/c/${col.path}`}
44
- viewLabel={t({ message: "View", comment: "@context: Button to view collection" })}
70
+ viewLabel={t({
71
+ message: "View",
72
+ comment: "@context: Button to view collection",
73
+ })}
45
74
  />
46
75
  }
47
76
  >
48
- <a href={`/dash/collections/${col.id}`} class="font-medium hover:underline">
77
+ <a
78
+ href={`/dash/collections/${col.id}`}
79
+ class="font-medium hover:underline"
80
+ >
49
81
  {col.title}
50
82
  </a>
51
83
  <p class="text-sm text-muted-foreground">/{col.path}</p>
52
84
  {col.description && (
53
- <p class="text-sm text-muted-foreground mt-1">{col.description}</p>
85
+ <p class="text-sm text-muted-foreground mt-1">
86
+ {col.description}
87
+ </p>
54
88
  )}
55
89
  </ListItemRow>
56
90
  ))}
@@ -64,40 +98,84 @@ function NewCollectionContent() {
64
98
  const { t } = useLingui();
65
99
  return (
66
100
  <>
67
- <h1 class="text-2xl font-semibold mb-6">{t({ message: "New Collection", comment: "@context: Page heading" })}</h1>
68
-
69
- <form method="post" action="/dash/collections" class="flex flex-col gap-4 max-w-lg">
101
+ <h1 class="text-2xl font-semibold mb-6">
102
+ {t({ message: "New Collection", comment: "@context: Page heading" })}
103
+ </h1>
104
+
105
+ <form
106
+ data-signals="{title: '', path: '', description: ''}"
107
+ data-on:submit__prevent="@post('/dash/collections')"
108
+ class="flex flex-col gap-4 max-w-lg"
109
+ >
70
110
  <div class="field">
71
- <label class="label">{t({ message: "Title", comment: "@context: Collection form field" })}</label>
72
- <input type="text" name="title" class="input" required placeholder={t({ message: "My Collection", comment: "@context: Collection title placeholder" })} />
111
+ <label class="label">
112
+ {t({
113
+ message: "Title",
114
+ comment: "@context: Collection form field",
115
+ })}
116
+ </label>
117
+ <input
118
+ type="text"
119
+ data-bind="title"
120
+ class="input"
121
+ required
122
+ placeholder={t({
123
+ message: "My Collection",
124
+ comment: "@context: Collection title placeholder",
125
+ })}
126
+ />
73
127
  </div>
74
128
 
75
129
  <div class="field">
76
- <label class="label">{t({ message: "Slug", comment: "@context: Collection form field" })}</label>
130
+ <label class="label">
131
+ {t({ message: "Slug", comment: "@context: Collection form field" })}
132
+ </label>
77
133
  <input
78
134
  type="text"
79
- name="path"
135
+ data-bind="path"
80
136
  class="input"
81
137
  required
82
138
  placeholder="my-collection"
83
139
  pattern="[a-z0-9-]+"
84
140
  />
85
141
  <p class="text-xs text-muted-foreground mt-1">
86
- {t({ message: "URL-safe identifier (lowercase, numbers, hyphens)", comment: "@context: Collection path help text" })}
142
+ {t({
143
+ message: "URL-safe identifier (lowercase, numbers, hyphens)",
144
+ comment: "@context: Collection path help text",
145
+ })}
87
146
  </p>
88
147
  </div>
89
148
 
90
149
  <div class="field">
91
- <label class="label">{t({ message: "Description (optional)", comment: "@context: Collection form field" })}</label>
92
- <textarea name="description" class="textarea" rows={3} placeholder={t({ message: "What's this collection about?", comment: "@context: Collection description placeholder" })} />
150
+ <label class="label">
151
+ {t({
152
+ message: "Description (optional)",
153
+ comment: "@context: Collection form field",
154
+ })}
155
+ </label>
156
+ <textarea
157
+ data-bind="description"
158
+ class="textarea"
159
+ rows={3}
160
+ placeholder={t({
161
+ message: "What's this collection about?",
162
+ comment: "@context: Collection description placeholder",
163
+ })}
164
+ />
93
165
  </div>
94
166
 
95
167
  <div class="flex gap-2">
96
168
  <button type="submit" class="btn">
97
- {t({ message: "Create Collection", comment: "@context: Button to save new collection" })}
169
+ {t({
170
+ message: "Create Collection",
171
+ comment: "@context: Button to save new collection",
172
+ })}
98
173
  </button>
99
174
  <a href="/dash/collections" class="btn-outline">
100
- {t({ message: "Cancel", comment: "@context: Button to cancel form" })}
175
+ {t({
176
+ message: "Cancel",
177
+ comment: "@context: Button to cancel form",
178
+ })}
101
179
  </a>
102
180
  </div>
103
181
  </form>
@@ -105,9 +183,19 @@ function NewCollectionContent() {
105
183
  );
106
184
  }
107
185
 
108
- function ViewCollectionContent({ collection, posts }: { collection: Collection; posts: Post[] }) {
186
+ function ViewCollectionContent({
187
+ collection,
188
+ posts,
189
+ }: {
190
+ collection: Collection;
191
+ posts: Post[];
192
+ }) {
109
193
  const { t } = useLingui();
110
- const postsHeader = t({ message: "Posts in Collection ({count})", comment: "@context: Collection posts section heading", values: { count: String(posts.length) } });
194
+ const postsHeader = t({
195
+ message: "Posts in Collection ({count})",
196
+ comment: "@context: Collection posts section heading",
197
+ values: { count: String(posts.length) },
198
+ });
111
199
 
112
200
  return (
113
201
  <>
@@ -118,9 +206,15 @@ function ViewCollectionContent({ collection, posts }: { collection: Collection;
118
206
  </div>
119
207
  <ActionButtons
120
208
  editHref={`/dash/collections/${collection.id}/edit`}
121
- editLabel={t({ message: "Edit", comment: "@context: Button to edit collection" })}
209
+ editLabel={t({
210
+ message: "Edit",
211
+ comment: "@context: Button to edit collection",
212
+ })}
122
213
  viewHref={`/c/${collection.path}`}
123
- viewLabel={t({ message: "View", comment: "@context: Button to view collection" })}
214
+ viewLabel={t({
215
+ message: "View",
216
+ comment: "@context: Button to view collection",
217
+ })}
124
218
  />
125
219
  </div>
126
220
 
@@ -134,7 +228,12 @@ function ViewCollectionContent({ collection, posts }: { collection: Collection;
134
228
  </header>
135
229
  <section>
136
230
  {posts.length === 0 ? (
137
- <p class="text-muted-foreground">{t({ message: "No posts in this collection.", comment: "@context: Empty state message" })}</p>
231
+ <p class="text-muted-foreground">
232
+ {t({
233
+ message: "No posts in this collection.",
234
+ comment: "@context: Empty state message",
235
+ })}
236
+ </p>
138
237
  ) : (
139
238
  <div class="flex flex-col divide-y">
140
239
  {posts.map((post) => (
@@ -144,15 +243,22 @@ function ViewCollectionContent({ collection, posts }: { collection: Collection;
144
243
  href={`/dash/posts/${sqid.encode(post.id)}`}
145
244
  class="font-medium hover:underline"
146
245
  >
147
- {post.title || post.content?.slice(0, 50) || `Post #${post.id}`}
246
+ {post.title ||
247
+ post.content?.slice(0, 50) ||
248
+ `Post #${post.id}`}
148
249
  </a>
149
250
  </div>
150
- <form method="post" action={`/dash/collections/${collection.id}/remove-post`}>
151
- <input type="hidden" name="postId" value={post.id} />
152
- <button type="submit" class="btn-sm-ghost text-destructive">
153
- {t({ message: "Remove", comment: "@context: Button to remove post from collection" })}
154
- </button>
155
- </form>
251
+ <button
252
+ type="button"
253
+ class="btn-sm-ghost text-destructive"
254
+ data-on:click__prevent={`@post('/dash/collections/${collection.id}/remove-post', {payload: {postId: ${post.id}}})`}
255
+ >
256
+ {t({
257
+ message: "Remove",
258
+ comment:
259
+ "@context: Button to remove post from collection",
260
+ })}
261
+ </button>
156
262
  </div>
157
263
  ))}
158
264
  </div>
@@ -162,7 +268,10 @@ function ViewCollectionContent({ collection, posts }: { collection: Collection;
162
268
 
163
269
  <div class="mt-6">
164
270
  <a href="/dash/collections" class="text-sm hover:underline">
165
- {t({ message: "← Back to Collections", comment: "@context: Navigation link" })}
271
+ {t({
272
+ message: "← Back to Collections",
273
+ comment: "@context: Navigation link",
274
+ })}
166
275
  </a>
167
276
  </div>
168
277
  </>
@@ -172,47 +281,79 @@ function ViewCollectionContent({ collection, posts }: { collection: Collection;
172
281
  function EditCollectionContent({ collection }: { collection: Collection }) {
173
282
  const { t } = useLingui();
174
283
 
284
+ const signals = JSON.stringify({
285
+ title: collection.title,
286
+ path: collection.path ?? "",
287
+ description: collection.description ?? "",
288
+ }).replace(/</g, "\\u003c");
289
+
175
290
  return (
176
291
  <>
177
- <h1 class="text-2xl font-semibold mb-6">{t({ message: "Edit Collection", comment: "@context: Page heading" })}</h1>
178
-
179
- <form method="post" action={`/dash/collections/${collection.id}`} class="flex flex-col gap-4 max-w-lg">
292
+ <h1 class="text-2xl font-semibold mb-6">
293
+ {t({ message: "Edit Collection", comment: "@context: Page heading" })}
294
+ </h1>
295
+
296
+ <form
297
+ data-signals={signals}
298
+ data-on:submit__prevent={`@post('/dash/collections/${collection.id}')`}
299
+ class="flex flex-col gap-4 max-w-lg"
300
+ >
180
301
  <div class="field">
181
- <label class="label">{t({ message: "Title", comment: "@context: Collection form field" })}</label>
182
- <input type="text" name="title" class="input" required value={collection.title} />
302
+ <label class="label">
303
+ {t({
304
+ message: "Title",
305
+ comment: "@context: Collection form field",
306
+ })}
307
+ </label>
308
+ <input type="text" data-bind="title" class="input" required />
183
309
  </div>
184
310
 
185
311
  <div class="field">
186
- <label class="label">{t({ message: "Slug", comment: "@context: Collection form field" })}</label>
312
+ <label class="label">
313
+ {t({ message: "Slug", comment: "@context: Collection form field" })}
314
+ </label>
187
315
  <input
188
316
  type="text"
189
- name="path"
317
+ data-bind="path"
190
318
  class="input"
191
319
  required
192
- value={collection.path ?? ""}
193
320
  pattern="[a-z0-9-]+"
194
321
  />
195
322
  </div>
196
323
 
197
324
  <div class="field">
198
- <label class="label">{t({ message: "Description (optional)", comment: "@context: Collection form field" })}</label>
199
- <textarea name="description" class="textarea" rows={3}>
325
+ <label class="label">
326
+ {t({
327
+ message: "Description (optional)",
328
+ comment: "@context: Collection form field",
329
+ })}
330
+ </label>
331
+ <textarea data-bind="description" class="textarea" rows={3}>
200
332
  {collection.description ?? ""}
201
333
  </textarea>
202
334
  </div>
203
335
 
204
336
  <div class="flex gap-2">
205
337
  <button type="submit" class="btn">
206
- {t({ message: "Update Collection", comment: "@context: Button to save collection changes" })}
338
+ {t({
339
+ message: "Update Collection",
340
+ comment: "@context: Button to save collection changes",
341
+ })}
207
342
  </button>
208
343
  <a href={`/dash/collections/${collection.id}`} class="btn-outline">
209
- {t({ message: "Cancel", comment: "@context: Button to cancel form" })}
344
+ {t({
345
+ message: "Cancel",
346
+ comment: "@context: Button to cancel form",
347
+ })}
210
348
  </a>
211
349
  </div>
212
350
  </form>
213
351
 
214
352
  <DangerZone
215
- actionLabel={t({ message: "Delete Collection", comment: "@context: Button to delete collection" })}
353
+ actionLabel={t({
354
+ message: "Delete Collection",
355
+ comment: "@context: Button to delete collection",
356
+ })}
216
357
  formAction={`/dash/collections/${collection.id}/delete`}
217
358
  confirmMessage="Are you sure you want to delete this collection?"
218
359
  />
@@ -226,9 +367,14 @@ collectionsRoutes.get("/", async (c) => {
226
367
  const collections = await c.var.services.collections.list();
227
368
 
228
369
  return c.html(
229
- <DashLayout c={c} title="Collections" siteName={siteName} currentPath="/dash/collections">
370
+ <DashLayout
371
+ c={c}
372
+ title="Collections"
373
+ siteName={siteName}
374
+ currentPath="/dash/collections"
375
+ >
230
376
  <CollectionsListContent collections={collections} />
231
- </DashLayout>
377
+ </DashLayout>,
232
378
  );
233
379
  });
234
380
 
@@ -237,27 +383,34 @@ collectionsRoutes.get("/new", async (c) => {
237
383
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
238
384
 
239
385
  return c.html(
240
- <DashLayout c={c} title="New Collection" siteName={siteName} currentPath="/dash/collections">
386
+ <DashLayout
387
+ c={c}
388
+ title="New Collection"
389
+ siteName={siteName}
390
+ currentPath="/dash/collections"
391
+ >
241
392
  <NewCollectionContent />
242
- </DashLayout>
393
+ </DashLayout>,
243
394
  );
244
395
  });
245
396
 
246
397
  // Create collection
247
398
  collectionsRoutes.post("/", async (c) => {
248
- const formData = await c.req.formData();
249
-
250
- const title = formData.get("title") as string;
251
- const path = formData.get("path") as string;
252
- const description = formData.get("description") as string;
399
+ const body = await c.req.json<{
400
+ title: string;
401
+ path: string;
402
+ description?: string;
403
+ }>();
253
404
 
254
405
  const collection = await c.var.services.collections.create({
255
- title,
256
- path,
257
- description: description || undefined,
406
+ title: body.title,
407
+ path: body.path,
408
+ description: body.description || undefined,
258
409
  });
259
410
 
260
- return c.redirect(`/dash/collections/${collection.id}`);
411
+ return sse(c, async (stream) => {
412
+ await stream.redirect(`/dash/collections/${collection.id}`);
413
+ });
261
414
  });
262
415
 
263
416
  // View single collection
@@ -272,9 +425,14 @@ collectionsRoutes.get("/:id", async (c) => {
272
425
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
273
426
 
274
427
  return c.html(
275
- <DashLayout c={c} title={collection.title} siteName={siteName} currentPath="/dash/collections">
428
+ <DashLayout
429
+ c={c}
430
+ title={collection.title}
431
+ siteName={siteName}
432
+ currentPath="/dash/collections"
433
+ >
276
434
  <ViewCollectionContent collection={collection} posts={posts} />
277
- </DashLayout>
435
+ </DashLayout>,
278
436
  );
279
437
  });
280
438
 
@@ -289,9 +447,14 @@ collectionsRoutes.get("/:id/edit", async (c) => {
289
447
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
290
448
 
291
449
  return c.html(
292
- <DashLayout c={c} title={`Edit: ${collection.title}`} siteName={siteName} currentPath="/dash/collections">
450
+ <DashLayout
451
+ c={c}
452
+ title={`Edit: ${collection.title}`}
453
+ siteName={siteName}
454
+ currentPath="/dash/collections"
455
+ >
293
456
  <EditCollectionContent collection={collection} />
294
- </DashLayout>
457
+ </DashLayout>,
295
458
  );
296
459
  });
297
460
 
@@ -300,19 +463,21 @@ collectionsRoutes.post("/:id", async (c) => {
300
463
  const id = parseInt(c.req.param("id"), 10);
301
464
  if (isNaN(id)) return c.notFound();
302
465
 
303
- const formData = await c.req.formData();
304
-
305
- const title = formData.get("title") as string;
306
- const path = formData.get("path") as string;
307
- const description = formData.get("description") as string;
466
+ const body = await c.req.json<{
467
+ title: string;
468
+ path: string;
469
+ description?: string;
470
+ }>();
308
471
 
309
472
  await c.var.services.collections.update(id, {
310
- title,
311
- path,
312
- description: description || undefined,
473
+ title: body.title,
474
+ path: body.path,
475
+ description: body.description || undefined,
313
476
  });
314
477
 
315
- return c.redirect(`/dash/collections/${id}`);
478
+ return sse(c, async (stream) => {
479
+ await stream.redirect(`/dash/collections/${id}`);
480
+ });
316
481
  });
317
482
 
318
483
  // Delete collection
@@ -322,7 +487,9 @@ collectionsRoutes.post("/:id/delete", async (c) => {
322
487
 
323
488
  await c.var.services.collections.delete(id);
324
489
 
325
- return c.redirect("/dash/collections");
490
+ return sse(c, async (stream) => {
491
+ await stream.redirect("/dash/collections");
492
+ });
326
493
  });
327
494
 
328
495
  // Remove post from collection
@@ -330,12 +497,13 @@ collectionsRoutes.post("/:id/remove-post", async (c) => {
330
497
  const id = parseInt(c.req.param("id"), 10);
331
498
  if (isNaN(id)) return c.notFound();
332
499
 
333
- const formData = await c.req.formData();
334
- const postId = parseInt(formData.get("postId") as string, 10);
500
+ const body = await c.req.json<{ postId: number }>();
335
501
 
336
- if (!isNaN(postId)) {
337
- await c.var.services.collections.removePost(id, postId);
502
+ if (body.postId) {
503
+ await c.var.services.collections.removePost(id, body.postId);
338
504
  }
339
505
 
340
- return c.redirect(`/dash/collections/${id}`);
506
+ return sse(c, async (stream) => {
507
+ await stream.redirect(`/dash/collections/${id}`);
508
+ });
341
509
  });
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Hono } from "hono";
8
- import { Trans, useLingui } from "../../i18n/index.js";
8
+ import { Trans, useLingui } from "@lingui/react/macro";
9
9
  import type { Bindings } from "../../types.js";
10
10
  import type { AppVariables } from "../../app.js";
11
11
  import { DashLayout } from "../../theme/layouts/index.js";
@@ -32,13 +32,19 @@ function DashboardContent({
32
32
  <div class="container py-8">
33
33
  <h1 class="text-2xl font-semibold mb-6">
34
34
  {/* ✅ No more nesting! */}
35
- {t({ message: "Dashboard", comment: "@context: Dashboard main heading" })}
35
+ {t({
36
+ message: "Dashboard",
37
+ comment: "@context: Dashboard main heading",
38
+ })}
36
39
  </h1>
37
40
 
38
41
  <div class="grid gap-4 md:grid-cols-3 mb-6">
39
42
  <div class="p-4 border rounded">
40
43
  <p class="text-sm text-muted-foreground">
41
- {t({ message: "Published", comment: "@context: Post status label" })}
44
+ {t({
45
+ message: "Published",
46
+ comment: "@context: Post status label",
47
+ })}
42
48
  </p>
43
49
  <p class="text-3xl font-bold">{publishedCount}</p>
44
50
  </div>
@@ -52,10 +58,16 @@ function DashboardContent({
52
58
 
53
59
  <div class="p-4 border rounded">
54
60
  <p class="text-sm text-muted-foreground mb-2">
55
- {t({ message: "Quick Actions", comment: "@context: Dashboard section title" })}
61
+ {t({
62
+ message: "Quick Actions",
63
+ comment: "@context: Dashboard section title",
64
+ })}
56
65
  </p>
57
66
  <a href="/dash/posts/new" class="btn btn-primary w-full">
58
- {t({ message: "New Post", comment: "@context: Button to create new post" })}
67
+ {t({
68
+ message: "New Post",
69
+ comment: "@context: Button to create new post",
70
+ })}
59
71
  </a>
60
72
  </div>
61
73
  </div>
@@ -63,7 +75,10 @@ function DashboardContent({
63
75
  {/* ✅ Trans component with embedded JSX! */}
64
76
  <p>
65
77
  <Trans comment="@context: Help text with link">
66
- Need help? Visit the <a href="/docs" class="underline">documentation</a>
78
+ Need help? Visit the{" "}
79
+ <a href="/docs" class="underline">
80
+ documentation
81
+ </a>
67
82
  </Trans>
68
83
  </p>
69
84
  </div>
@@ -84,6 +99,6 @@ dashIndexRoutes.get("/", async (c) => {
84
99
  publishedCount={publishedPosts.length}
85
100
  draftCount={draftPosts.length}
86
101
  />
87
- </DashLayout>
102
+ </DashLayout>,
88
103
  );
89
104
  });