@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
@@ -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,290 @@ 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="mb-4 max-w-lg rounded-md border border-green-200 bg-green-50 p-3 text-sm text-green-800 transition-opacity duration-300 dark:border-green-800 dark:bg-green-950 dark:text-green-200"
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
+ {t({
48
+ message: "Settings saved successfully.",
49
+ comment: "@context: Toast message after saving settings",
50
+ })}
71
51
  </div>
52
+ )}
53
+
54
+ <div class="flex flex-col gap-6 max-w-lg">
55
+ <form
56
+ data-signals={generalSignals}
57
+ data-on:submit__prevent="@post('/dash/settings')"
58
+ >
59
+ <div id="settings-message"></div>
60
+ <div class="card">
61
+ <header>
62
+ <h2>
63
+ {t({
64
+ message: "General",
65
+ comment: "@context: Settings section heading",
66
+ })}
67
+ </h2>
68
+ </header>
69
+ <section class="flex flex-col gap-4">
70
+ <div class="field">
71
+ <label class="label">
72
+ {t({
73
+ message: "Site Name",
74
+ comment: "@context: Settings form field",
75
+ })}
76
+ </label>
77
+ <input
78
+ type="text"
79
+ data-bind="siteName"
80
+ class="input"
81
+ required
82
+ />
83
+ </div>
84
+
85
+ <div class="field">
86
+ <label class="label">
87
+ {t({
88
+ message: "Site Description",
89
+ comment: "@context: Settings form field",
90
+ })}
91
+ </label>
92
+ <textarea data-bind="siteDescription" class="textarea" rows={3}>
93
+ {siteDescription}
94
+ </textarea>
95
+ </div>
96
+
97
+ <div class="field">
98
+ <label class="label">
99
+ {t({
100
+ message: "Language",
101
+ comment: "@context: Settings form field",
102
+ })}
103
+ </label>
104
+ <select data-bind="siteLanguage" class="select">
105
+ <option value="en" selected={siteLanguage === "en"}>
106
+ English
107
+ </option>
108
+ <option value="zh-Hans" selected={siteLanguage === "zh-Hans"}>
109
+ 简体中文
110
+ </option>
111
+ <option value="zh-Hant" selected={siteLanguage === "zh-Hant"}>
112
+ 繁體中文
113
+ </option>
114
+ </select>
115
+ </div>
116
+ </section>
117
+ </div>
118
+
119
+ <button type="submit" class="btn mt-4">
120
+ {t({
121
+ message: "Save Settings",
122
+ comment: "@context: Button to save settings",
123
+ })}
124
+ </button>
125
+ </form>
126
+
127
+ <form
128
+ data-signals="{currentPassword: '', newPassword: '', confirmPassword: ''}"
129
+ data-on:submit__prevent="@post('/dash/settings/password')"
130
+ >
131
+ <div id="password-message"></div>
132
+ <div class="card">
133
+ <header>
134
+ <h2>
135
+ {t({
136
+ message: "Change Password",
137
+ comment: "@context: Settings section heading",
138
+ })}
139
+ </h2>
140
+ </header>
141
+ <section class="flex flex-col gap-4">
142
+ <div class="field">
143
+ <label class="label">
144
+ {t({
145
+ message: "Current Password",
146
+ comment: "@context: Password form field",
147
+ })}
148
+ </label>
149
+ <input
150
+ type="password"
151
+ data-bind="currentPassword"
152
+ class="input"
153
+ required
154
+ autocomplete="current-password"
155
+ />
156
+ </div>
157
+
158
+ <div class="field">
159
+ <label class="label">
160
+ {t({
161
+ message: "New Password",
162
+ comment: "@context: Password form field",
163
+ })}
164
+ </label>
165
+ <input
166
+ type="password"
167
+ data-bind="newPassword"
168
+ class="input"
169
+ required
170
+ minlength={8}
171
+ autocomplete="new-password"
172
+ />
173
+ </div>
174
+
175
+ <div class="field">
176
+ <label class="label">
177
+ {t({
178
+ message: "Confirm New Password",
179
+ comment: "@context: Password form field",
180
+ })}
181
+ </label>
182
+ <input
183
+ type="password"
184
+ data-bind="confirmPassword"
185
+ class="input"
186
+ required
187
+ minlength={8}
188
+ autocomplete="new-password"
189
+ />
190
+ </div>
191
+ </section>
192
+ </div>
72
193
 
73
- <button type="submit" class="btn">
74
- {t({ message: "Save Settings", comment: "@context: Button to save settings" })}
75
- </button>
76
- </form>
194
+ <button type="submit" class="btn mt-4">
195
+ {t({
196
+ message: "Change Password",
197
+ comment: "@context: Button to change password",
198
+ })}
199
+ </button>
200
+ </form>
201
+ </div>
77
202
  </>
78
203
  );
79
204
  }
80
205
 
81
206
  // Settings page
82
207
  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";
208
+ const all = await c.var.services.settings.getAll();
209
+ const siteName = all["SITE_NAME"] ?? "Jant";
210
+ const siteDescription = all["SITE_DESCRIPTION"] ?? "";
211
+ const siteLanguage = all["SITE_LANGUAGE"] ?? "en";
212
+ const saved = c.req.query("saved") !== undefined;
86
213
 
87
214
  return c.html(
88
- <DashLayout c={c} title="Settings" siteName={siteName} currentPath="/dash/settings">
215
+ <DashLayout
216
+ c={c}
217
+ title="Settings"
218
+ siteName={siteName}
219
+ currentPath="/dash/settings"
220
+ >
89
221
  <SettingsContent
90
222
  siteName={siteName}
91
223
  siteDescription={siteDescription}
92
224
  siteLanguage={siteLanguage}
225
+ saved={saved}
93
226
  />
94
- </DashLayout>
227
+ </DashLayout>,
95
228
  );
96
229
  });
97
230
 
98
231
  // Update settings
99
232
  settingsRoutes.post("/", async (c) => {
100
- const formData = await c.req.formData();
233
+ const body = await c.req.json<{
234
+ siteName: string;
235
+ siteDescription: string;
236
+ siteLanguage: string;
237
+ }>();
101
238
 
102
- const siteName = formData.get("siteName") as string;
103
- const siteDescription = formData.get("siteDescription") as string;
104
- const siteLanguage = formData.get("siteLanguage") as string;
239
+ const oldLanguage =
240
+ (await c.var.services.settings.get("SITE_LANGUAGE")) ?? "en";
105
241
 
106
242
  await c.var.services.settings.setMany({
107
- SITE_NAME: siteName,
108
- SITE_DESCRIPTION: siteDescription,
109
- SITE_LANGUAGE: siteLanguage,
243
+ SITE_NAME: body.siteName,
244
+ SITE_DESCRIPTION: body.siteDescription,
245
+ SITE_LANGUAGE: body.siteLanguage,
110
246
  });
111
247
 
112
- return c.redirect("/dash/settings?saved=1");
248
+ const languageChanged = oldLanguage !== body.siteLanguage;
249
+
250
+ return sse(c, async (stream) => {
251
+ if (languageChanged) {
252
+ // Language changed - full reload needed to update all UI text
253
+ await stream.redirect("/dash/settings?saved");
254
+ } else {
255
+ // No language change - show inline success message
256
+ await stream.patchElements(
257
+ '<div id="settings-message"><div class="rounded-md border border-green-200 bg-green-50 p-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200 mb-4 transition-opacity duration-300" data-init="setTimeout(() => { el.style.opacity = \'0\'; setTimeout(() => el.remove(), 300) }, 3000)">Settings saved successfully.</div></div>',
258
+ );
259
+ }
260
+ });
261
+ });
262
+
263
+ // Change password
264
+ settingsRoutes.post("/password", async (c) => {
265
+ const body = await c.req.json<{
266
+ currentPassword: string;
267
+ newPassword: string;
268
+ confirmPassword: string;
269
+ }>();
270
+
271
+ if (body.newPassword !== body.confirmPassword) {
272
+ return sse(c, async (stream) => {
273
+ await stream.patchElements(
274
+ '<div id="password-message"><div class="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200 mb-4">Passwords do not match.</div></div>',
275
+ );
276
+ });
277
+ }
278
+
279
+ try {
280
+ await c.var.auth.api.changePassword({
281
+ body: {
282
+ currentPassword: body.currentPassword,
283
+ newPassword: body.newPassword,
284
+ revokeOtherSessions: false,
285
+ },
286
+ headers: c.req.raw.headers,
287
+ });
288
+ } catch {
289
+ return sse(c, async (stream) => {
290
+ await stream.patchElements(
291
+ '<div id="password-message"><div class="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200 mb-4">Current password is incorrect.</div></div>',
292
+ );
293
+ });
294
+ }
295
+
296
+ return sse(c, async (stream) => {
297
+ await stream.patchElements(
298
+ '<div id="password-message"><div class="rounded-md border border-green-200 bg-green-50 p-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200 mb-4">Password changed successfully.</div></div>',
299
+ );
300
+ await stream.patchSignals({
301
+ currentPassword: "",
302
+ newPassword: "",
303
+ confirmPassword: "",
304
+ });
305
+ });
113
306
  });
@@ -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
  });