@jant/core 0.2.12 → 0.2.14

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 +251 -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 +29 -29
  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 +252 -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
@@ -3,10 +3,11 @@
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 } from "../../types.js";
8
8
  import type { AppVariables } from "../../app.js";
9
9
  import { DashLayout } from "../../theme/layouts/index.js";
10
+ import { sse } from "../../lib/sse.js";
10
11
 
11
12
  type Env = { Bindings: Bindings; Variables: AppVariables };
12
13
 
@@ -16,98 +17,292 @@ function SettingsContent({
16
17
  siteName,
17
18
  siteDescription,
18
19
  siteLanguage,
20
+ saved,
19
21
  }: {
20
22
  siteName: string;
21
23
  siteDescription: string;
22
24
  siteLanguage: string;
25
+ saved: boolean;
23
26
  }) {
24
27
  const { t } = useLingui();
25
28
 
29
+ const generalSignals = JSON.stringify({
30
+ siteName,
31
+ siteDescription,
32
+ siteLanguage,
33
+ }).replace(/</g, "\\u003c");
34
+
26
35
  return (
27
36
  <>
28
37
  <h1 class="text-2xl font-semibold mb-6">
29
38
  {t({ message: "Settings", comment: "@context: Dashboard heading" })}
30
39
  </h1>
31
40
 
32
- <form method="post" action="/dash/settings" class="flex flex-col gap-6 max-w-lg">
33
- <div class="card">
34
- <header>
35
- <h2>{t({ message: "General", comment: "@context: Settings section heading" })}</h2>
36
- </header>
37
- <section class="flex flex-col gap-4">
38
- <div class="field">
39
- <label class="label">
40
- {t({ message: "Site Name", comment: "@context: Settings form field" })}
41
- </label>
42
- <input type="text" name="siteName" class="input" value={siteName} required />
43
- </div>
44
-
45
- <div class="field">
46
- <label class="label">
47
- {t({ message: "Site Description", comment: "@context: Settings form field" })}
48
- </label>
49
- <textarea name="siteDescription" class="textarea" rows={3}>
50
- {siteDescription}
51
- </textarea>
52
- </div>
53
-
54
- <div class="field">
55
- <label class="label">
56
- {t({ message: "Language", comment: "@context: Settings form field" })}
57
- </label>
58
- <select name="siteLanguage" class="select">
59
- <option value="en" selected={siteLanguage === "en"}>
60
- English
61
- </option>
62
- <option value="zh-Hans" selected={siteLanguage === "zh-Hans"}>
63
- 简体中文
64
- </option>
65
- <option value="zh-Hant" selected={siteLanguage === "zh-Hant"}>
66
- 繁體中文
67
- </option>
68
- </select>
69
- </div>
70
- </section>
41
+ {saved && (
42
+ <div
43
+ id="settings-saved-toast"
44
+ class="alert mb-4 max-w-lg transition-opacity duration-300"
45
+ data-init={`console.log('[toast] init fired at', Date.now()); history.replaceState({}, '', '/dash/settings'); setTimeout(() => { console.log('[toast] hiding at', Date.now()); const el = document.getElementById('settings-saved-toast'); if (el) { el.style.opacity = '0'; setTimeout(() => el.remove(), 300) } }, 3000)`}
46
+ >
47
+ <h2>
48
+ {t({
49
+ message: "Settings saved successfully.",
50
+ comment: "@context: Toast message after saving settings",
51
+ })}
52
+ </h2>
71
53
  </div>
54
+ )}
55
+
56
+ <div class="flex flex-col gap-6 max-w-lg">
57
+ <form
58
+ data-signals={generalSignals}
59
+ data-on:submit__prevent="@post('/dash/settings')"
60
+ >
61
+ <div id="settings-message"></div>
62
+ <div class="card">
63
+ <header>
64
+ <h2>
65
+ {t({
66
+ message: "General",
67
+ comment: "@context: Settings section heading",
68
+ })}
69
+ </h2>
70
+ </header>
71
+ <section class="flex flex-col gap-4">
72
+ <div class="field">
73
+ <label class="label">
74
+ {t({
75
+ message: "Site Name",
76
+ comment: "@context: Settings form field",
77
+ })}
78
+ </label>
79
+ <input
80
+ type="text"
81
+ data-bind="siteName"
82
+ class="input"
83
+ required
84
+ />
85
+ </div>
86
+
87
+ <div class="field">
88
+ <label class="label">
89
+ {t({
90
+ message: "Site Description",
91
+ comment: "@context: Settings form field",
92
+ })}
93
+ </label>
94
+ <textarea data-bind="siteDescription" class="textarea" rows={3}>
95
+ {siteDescription}
96
+ </textarea>
97
+ </div>
98
+
99
+ <div class="field">
100
+ <label class="label">
101
+ {t({
102
+ message: "Language",
103
+ comment: "@context: Settings form field",
104
+ })}
105
+ </label>
106
+ <select data-bind="siteLanguage" class="select">
107
+ <option value="en" selected={siteLanguage === "en"}>
108
+ English
109
+ </option>
110
+ <option value="zh-Hans" selected={siteLanguage === "zh-Hans"}>
111
+ 简体中文
112
+ </option>
113
+ <option value="zh-Hant" selected={siteLanguage === "zh-Hant"}>
114
+ 繁體中文
115
+ </option>
116
+ </select>
117
+ </div>
118
+ </section>
119
+ </div>
120
+
121
+ <button type="submit" class="btn mt-4">
122
+ {t({
123
+ message: "Save Settings",
124
+ comment: "@context: Button to save settings",
125
+ })}
126
+ </button>
127
+ </form>
128
+
129
+ <form
130
+ data-signals="{currentPassword: '', newPassword: '', confirmPassword: ''}"
131
+ data-on:submit__prevent="@post('/dash/settings/password')"
132
+ >
133
+ <div id="password-message"></div>
134
+ <div class="card">
135
+ <header>
136
+ <h2>
137
+ {t({
138
+ message: "Change Password",
139
+ comment: "@context: Settings section heading",
140
+ })}
141
+ </h2>
142
+ </header>
143
+ <section class="flex flex-col gap-4">
144
+ <div class="field">
145
+ <label class="label">
146
+ {t({
147
+ message: "Current Password",
148
+ comment: "@context: Password form field",
149
+ })}
150
+ </label>
151
+ <input
152
+ type="password"
153
+ data-bind="currentPassword"
154
+ class="input"
155
+ required
156
+ autocomplete="current-password"
157
+ />
158
+ </div>
159
+
160
+ <div class="field">
161
+ <label class="label">
162
+ {t({
163
+ message: "New Password",
164
+ comment: "@context: Password form field",
165
+ })}
166
+ </label>
167
+ <input
168
+ type="password"
169
+ data-bind="newPassword"
170
+ class="input"
171
+ required
172
+ minlength={8}
173
+ autocomplete="new-password"
174
+ />
175
+ </div>
176
+
177
+ <div class="field">
178
+ <label class="label">
179
+ {t({
180
+ message: "Confirm New Password",
181
+ comment: "@context: Password form field",
182
+ })}
183
+ </label>
184
+ <input
185
+ type="password"
186
+ data-bind="confirmPassword"
187
+ class="input"
188
+ required
189
+ minlength={8}
190
+ autocomplete="new-password"
191
+ />
192
+ </div>
193
+ </section>
194
+ </div>
72
195
 
73
- <button type="submit" class="btn">
74
- {t({ message: "Save Settings", comment: "@context: Button to save settings" })}
75
- </button>
76
- </form>
196
+ <button type="submit" class="btn mt-4">
197
+ {t({
198
+ message: "Change Password",
199
+ comment: "@context: Button to change password",
200
+ })}
201
+ </button>
202
+ </form>
203
+ </div>
77
204
  </>
78
205
  );
79
206
  }
80
207
 
81
208
  // Settings page
82
209
  settingsRoutes.get("/", async (c) => {
83
- const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
84
- const siteDescription = (await c.var.services.settings.get("SITE_DESCRIPTION")) ?? "";
85
- const siteLanguage = (await c.var.services.settings.get("SITE_LANGUAGE")) ?? "en";
210
+ const all = await c.var.services.settings.getAll();
211
+ const siteName = all["SITE_NAME"] ?? "Jant";
212
+ const siteDescription = all["SITE_DESCRIPTION"] ?? "";
213
+ const siteLanguage = all["SITE_LANGUAGE"] ?? "en";
214
+ const saved = c.req.query("saved") !== undefined;
86
215
 
87
216
  return c.html(
88
- <DashLayout c={c} title="Settings" siteName={siteName} currentPath="/dash/settings">
217
+ <DashLayout
218
+ c={c}
219
+ title="Settings"
220
+ siteName={siteName}
221
+ currentPath="/dash/settings"
222
+ >
89
223
  <SettingsContent
90
224
  siteName={siteName}
91
225
  siteDescription={siteDescription}
92
226
  siteLanguage={siteLanguage}
227
+ saved={saved}
93
228
  />
94
- </DashLayout>
229
+ </DashLayout>,
95
230
  );
96
231
  });
97
232
 
98
233
  // Update settings
99
234
  settingsRoutes.post("/", async (c) => {
100
- const formData = await c.req.formData();
235
+ const body = await c.req.json<{
236
+ siteName: string;
237
+ siteDescription: string;
238
+ siteLanguage: string;
239
+ }>();
101
240
 
102
- const siteName = formData.get("siteName") as string;
103
- const siteDescription = formData.get("siteDescription") as string;
104
- const siteLanguage = formData.get("siteLanguage") as string;
241
+ const oldLanguage =
242
+ (await c.var.services.settings.get("SITE_LANGUAGE")) ?? "en";
105
243
 
106
244
  await c.var.services.settings.setMany({
107
- SITE_NAME: siteName,
108
- SITE_DESCRIPTION: siteDescription,
109
- SITE_LANGUAGE: siteLanguage,
245
+ SITE_NAME: body.siteName,
246
+ SITE_DESCRIPTION: body.siteDescription,
247
+ SITE_LANGUAGE: body.siteLanguage,
110
248
  });
111
249
 
112
- return c.redirect("/dash/settings?saved=1");
250
+ const languageChanged = oldLanguage !== body.siteLanguage;
251
+
252
+ return sse(c, async (stream) => {
253
+ if (languageChanged) {
254
+ // Language changed - full reload needed to update all UI text
255
+ await stream.redirect("/dash/settings?saved");
256
+ } else {
257
+ // No language change - show inline success message
258
+ await stream.patchElements(
259
+ '<div id="settings-message"><div class="alert mb-4 transition-opacity duration-300" data-init="setTimeout(() => { el.style.opacity = \'0\'; setTimeout(() => el.remove(), 300) }, 3000)"><h2>Settings saved successfully.</h2></div></div>',
260
+ );
261
+ }
262
+ });
263
+ });
264
+
265
+ // Change password
266
+ settingsRoutes.post("/password", async (c) => {
267
+ const body = await c.req.json<{
268
+ currentPassword: string;
269
+ newPassword: string;
270
+ confirmPassword: string;
271
+ }>();
272
+
273
+ if (body.newPassword !== body.confirmPassword) {
274
+ return sse(c, async (stream) => {
275
+ await stream.patchElements(
276
+ '<div id="password-message"><div class="alert-destructive mb-4"><h2>Passwords do not match.</h2></div></div>',
277
+ );
278
+ });
279
+ }
280
+
281
+ try {
282
+ await c.var.auth.api.changePassword({
283
+ body: {
284
+ currentPassword: body.currentPassword,
285
+ newPassword: body.newPassword,
286
+ revokeOtherSessions: false,
287
+ },
288
+ headers: c.req.raw.headers,
289
+ });
290
+ } catch {
291
+ return sse(c, async (stream) => {
292
+ await stream.patchElements(
293
+ '<div id="password-message"><div class="alert-destructive mb-4"><h2>Current password is incorrect.</h2></div></div>',
294
+ );
295
+ });
296
+ }
297
+
298
+ return sse(c, async (stream) => {
299
+ await stream.patchElements(
300
+ '<div id="password-message"><div class="alert mb-4"><h2>Password changed successfully.</h2></div></div>',
301
+ );
302
+ await stream.patchSignals({
303
+ currentPassword: "",
304
+ newPassword: "",
305
+ confirmPassword: "",
306
+ });
307
+ });
113
308
  });
@@ -14,8 +14,9 @@ export const rssRoutes = new Hono<Env>();
14
14
 
15
15
  // RSS 2.0 Feed - main feed at /feed
16
16
  rssRoutes.get("/", async (c) => {
17
- const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
18
- const siteDescription = (await c.var.services.settings.get("SITE_DESCRIPTION")) ?? "";
17
+ const all = await c.var.services.settings.getAll();
18
+ const siteName = all["SITE_NAME"] ?? "Jant";
19
+ const siteDescription = all["SITE_DESCRIPTION"] ?? "";
19
20
  const siteUrl = c.env.SITE_URL;
20
21
 
21
22
  const posts = await c.var.services.posts.list({
@@ -61,8 +62,9 @@ rssRoutes.get("/", async (c) => {
61
62
 
62
63
  // Atom Feed
63
64
  rssRoutes.get("/atom.xml", async (c) => {
64
- const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
65
- const siteDescription = (await c.var.services.settings.get("SITE_DESCRIPTION")) ?? "";
65
+ const all = await c.var.services.settings.getAll();
66
+ const siteName = all["SITE_NAME"] ?? "Jant";
67
+ const siteDescription = all["SITE_DESCRIPTION"] ?? "";
66
68
  const siteUrl = c.env.SITE_URL;
67
69
 
68
70
  const posts = await c.var.services.posts.list({
@@ -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, PostType } from "../../types.js";
10
10
  import type { AppVariables } from "../../app.js";
11
11
  import { BaseLayout } from "../../theme/layouts/index.js";
@@ -24,10 +24,19 @@ function getTypeLabel(type: string): string {
24
24
  const { t } = useLingui();
25
25
  const labels: Record<string, string> = {
26
26
  note: t({ message: "Note", comment: "@context: Post type label - note" }),
27
- article: t({ message: "Article", comment: "@context: Post type label - article" }),
27
+ article: t({
28
+ message: "Article",
29
+ comment: "@context: Post type label - article",
30
+ }),
28
31
  link: t({ message: "Link", comment: "@context: Post type label - link" }),
29
- quote: t({ message: "Quote", comment: "@context: Post type label - quote" }),
30
- image: t({ message: "Image", comment: "@context: Post type label - image" }),
32
+ quote: t({
33
+ message: "Quote",
34
+ comment: "@context: Post type label - quote",
35
+ }),
36
+ image: t({
37
+ message: "Image",
38
+ comment: "@context: Post type label - image",
39
+ }),
31
40
  page: t({ message: "Page", comment: "@context: Post type label - page" }),
32
41
  };
33
42
  return labels[type] ?? type;
@@ -36,12 +45,30 @@ function getTypeLabel(type: string): string {
36
45
  function getTypeLabelPlural(type: string): string {
37
46
  const { t } = useLingui();
38
47
  const labels: Record<string, string> = {
39
- note: t({ message: "Notes", comment: "@context: Post type label plural - notes" }),
40
- article: t({ message: "Articles", comment: "@context: Post type label plural - articles" }),
41
- link: t({ message: "Links", comment: "@context: Post type label plural - links" }),
42
- quote: t({ message: "Quotes", comment: "@context: Post type label plural - quotes" }),
43
- image: t({ message: "Images", comment: "@context: Post type label plural - images" }),
44
- page: t({ message: "Pages", comment: "@context: Post type label plural - pages" }),
48
+ note: t({
49
+ message: "Notes",
50
+ comment: "@context: Post type label plural - notes",
51
+ }),
52
+ article: t({
53
+ message: "Articles",
54
+ comment: "@context: Post type label plural - articles",
55
+ }),
56
+ link: t({
57
+ message: "Links",
58
+ comment: "@context: Post type label plural - links",
59
+ }),
60
+ quote: t({
61
+ message: "Quotes",
62
+ comment: "@context: Post type label plural - quotes",
63
+ }),
64
+ image: t({
65
+ message: "Images",
66
+ comment: "@context: Post type label plural - images",
67
+ }),
68
+ page: t({
69
+ message: "Pages",
70
+ comment: "@context: Post type label plural - pages",
71
+ }),
45
72
  };
46
73
  return labels[type] ?? `${type}s`;
47
74
  }
@@ -80,8 +107,14 @@ function ArchiveContent({
80
107
 
81
108
  {/* Type filter */}
82
109
  <nav class="flex flex-wrap gap-2 mt-4">
83
- <a href="/archive" class={`badge ${!type ? "badge-primary" : "badge-outline"}`}>
84
- {t({ message: "All", comment: "@context: Archive filter - all types" })}
110
+ <a
111
+ href="/archive"
112
+ class={`badge ${!type ? "badge-primary" : "badge-outline"}`}
113
+ >
114
+ {t({
115
+ message: "All",
116
+ comment: "@context: Archive filter - all types",
117
+ })}
85
118
  </a>
86
119
  {POST_TYPES.filter((t) => t !== "page").map((typeKey) => (
87
120
  <a
@@ -98,7 +131,10 @@ function ArchiveContent({
98
131
  <main>
99
132
  {displayPosts.length === 0 ? (
100
133
  <p class="text-muted-foreground">
101
- {t({ message: "No posts found.", comment: "@context: Archive empty state" })}
134
+ {t({
135
+ message: "No posts found.",
136
+ comment: "@context: Archive empty state",
137
+ })}
102
138
  </p>
103
139
  ) : (
104
140
  Array.from(grouped.entries()).map(([yearMonth, monthPosts]) => (
@@ -118,11 +154,18 @@ function ArchiveContent({
118
154
  {new Date(post.publishedAt * 1000).getDate()}
119
155
  </time>
120
156
  <div class="flex-1 min-w-0">
121
- <a href={`/p/${sqid.encode(post.id)}`} class="hover:underline">
122
- {post.title || post.content?.slice(0, 80) || `Post #${post.id}`}
157
+ <a
158
+ href={`/p/${sqid.encode(post.id)}`}
159
+ class="hover:underline"
160
+ >
161
+ {post.title ||
162
+ post.content?.slice(0, 80) ||
163
+ `Post #${post.id}`}
123
164
  </a>
124
165
  {!type && (
125
- <span class="ml-2 badge-outline text-xs">{getTypeLabel(post.type)}</span>
166
+ <span class="ml-2 badge-outline text-xs">
167
+ {getTypeLabel(post.type)}
168
+ </span>
126
169
  )}
127
170
  {replyCount && replyCount > 0 && (
128
171
  <span class="ml-2 text-xs text-muted-foreground">
@@ -130,11 +173,13 @@ function ArchiveContent({
130
173
  {replyCount === 1
131
174
  ? t({
132
175
  message: "1 reply",
133
- comment: "@context: Archive post reply indicator - single",
176
+ comment:
177
+ "@context: Archive post reply indicator - single",
134
178
  })
135
179
  : t({
136
180
  message: "{count} replies",
137
- comment: "@context: Archive post reply indicator - plural",
181
+ comment:
182
+ "@context: Archive post reply indicator - plural",
138
183
  values: { count: String(replyCount) },
139
184
  })}
140
185
  )
@@ -159,7 +204,11 @@ function ArchiveContent({
159
204
 
160
205
  <nav class="mt-4">
161
206
  <a href="/" class="text-sm hover:underline">
162
- {t({ message: "Back to home", comment: "@context: Navigation link back to home page" })}
207
+ ←{" "}
208
+ {t({
209
+ message: "Back to home",
210
+ comment: "@context: Navigation link back to home page",
211
+ })}
163
212
  </a>
164
213
  </nav>
165
214
  </div>
@@ -169,7 +218,8 @@ function ArchiveContent({
169
218
  // Archive page - all posts
170
219
  archiveRoutes.get("/", async (c) => {
171
220
  const typeParam = c.req.query("type") as PostType | undefined;
172
- const type = typeParam && POST_TYPES.includes(typeParam) ? typeParam : undefined;
221
+ const type =
222
+ typeParam && POST_TYPES.includes(typeParam) ? typeParam : undefined;
173
223
 
174
224
  // Parse cursor
175
225
  const cursorParam = c.req.query("cursor");
@@ -222,6 +272,6 @@ archiveRoutes.get("/", async (c) => {
222
272
  grouped={grouped}
223
273
  replyCounts={replyCounts}
224
274
  />
225
- </BaseLayout>
275
+ </BaseLayout>,
226
276
  );
227
277
  });
@@ -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, Collection, Post } from "../../types.js";
8
8
  import type { AppVariables } from "../../app.js";
9
9
  import { BaseLayout } from "../../theme/layouts/index.js";
@@ -14,7 +14,13 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
14
14
 
15
15
  export const collectionRoutes = new Hono<Env>();
16
16
 
17
- function CollectionContent({ collection, posts }: { collection: Collection; posts: Post[] }) {
17
+ function CollectionContent({
18
+ collection,
19
+ posts,
20
+ }: {
21
+ collection: Collection;
22
+ posts: Post[];
23
+ }) {
18
24
  const { t } = useLingui();
19
25
 
20
26
  return (
@@ -39,7 +45,10 @@ function CollectionContent({ collection, posts }: { collection: Collection; post
39
45
  <article key={post.id} class="h-entry">
40
46
  {post.title && (
41
47
  <h2 class="p-name text-lg font-medium mb-2">
42
- <a href={`/p/${sqid.encode(post.id)}`} class="u-url hover:underline">
48
+ <a
49
+ href={`/p/${sqid.encode(post.id)}`}
50
+ class="u-url hover:underline"
51
+ >
43
52
  {post.title}
44
53
  </a>
45
54
  </h2>
@@ -49,7 +58,10 @@ function CollectionContent({ collection, posts }: { collection: Collection; post
49
58
  dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
50
59
  />
51
60
  <footer class="mt-2 text-sm text-muted-foreground">
52
- <time class="dt-published" datetime={time.toISOString(post.publishedAt)}>
61
+ <time
62
+ class="dt-published"
63
+ datetime={time.toISOString(post.publishedAt)}
64
+ >
53
65
  {time.formatDate(post.publishedAt)}
54
66
  </time>
55
67
  </footer>
@@ -60,7 +72,10 @@ function CollectionContent({ collection, posts }: { collection: Collection; post
60
72
 
61
73
  <nav class="mt-8">
62
74
  <a href="/" class="text-sm hover:underline">
63
- {t({ message: "← Back to home", comment: "@context: Navigation link" })}
75
+ {t({
76
+ message: "← Back to home",
77
+ comment: "@context: Navigation link",
78
+ })}
64
79
  </a>
65
80
  </nav>
66
81
  </div>
@@ -83,6 +98,6 @@ collectionRoutes.get("/:path", async (c) => {
83
98
  c={c}
84
99
  >
85
100
  <CollectionContent collection={collection} posts={posts} />
86
- </BaseLayout>
101
+ </BaseLayout>,
87
102
  );
88
103
  });