@jant/core 0.3.26 → 0.3.28

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 (314) hide show
  1. package/dist/client/client.css +1 -0
  2. package/dist/client/client.js +31561 -0
  3. package/dist/index.js +15209 -15
  4. package/package.json +21 -15
  5. package/src/__tests__/helpers/app.ts +19 -3
  6. package/src/__tests__/helpers/db.ts +44 -0
  7. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  8. package/src/app.tsx +112 -173
  9. package/src/auth.ts +4 -1
  10. package/src/client.ts +13 -0
  11. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  12. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  13. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  14. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  15. package/src/db/schema.ts +24 -4
  16. package/src/i18n/locales/en.po +810 -385
  17. package/src/i18n/locales/en.ts +1 -1
  18. package/src/i18n/locales/zh-Hans.po +733 -522
  19. package/src/i18n/locales/zh-Hans.ts +1 -1
  20. package/src/i18n/locales/zh-Hant.po +733 -522
  21. package/src/i18n/locales/zh-Hant.ts +1 -1
  22. package/src/i18n/middleware.ts +7 -11
  23. package/src/index.ts +1 -1
  24. package/src/lib/__tests__/icons.test.ts +178 -0
  25. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  26. package/src/lib/__tests__/schemas.test.ts +12 -6
  27. package/src/lib/__tests__/theme.test.ts +62 -0
  28. package/src/lib/__tests__/timezones.test.ts +1 -1
  29. package/src/lib/__tests__/url.test.ts +12 -0
  30. package/src/lib/__tests__/view.test.ts +1 -5
  31. package/src/lib/avatar-upload.ts +18 -10
  32. package/src/lib/collection-form-bridge.ts +52 -0
  33. package/src/lib/collections-reorder.ts +28 -0
  34. package/src/lib/compose-bridge.ts +251 -0
  35. package/src/lib/errors.ts +116 -0
  36. package/src/lib/excerpt.ts +1 -1
  37. package/src/lib/favicon.ts +3 -5
  38. package/src/lib/html.ts +22 -0
  39. package/src/lib/icon-catalog.ts +181 -0
  40. package/src/lib/icons.ts +202 -0
  41. package/src/lib/navigation.ts +18 -33
  42. package/src/lib/pagination.ts +3 -2
  43. package/src/lib/post-form-bridge.ts +136 -0
  44. package/src/lib/render.tsx +11 -4
  45. package/src/lib/resolve-config.ts +157 -0
  46. package/src/lib/schemas.ts +76 -12
  47. package/src/lib/settings-bridge.ts +139 -0
  48. package/src/lib/storage.ts +37 -16
  49. package/src/lib/theme.ts +5 -7
  50. package/src/lib/timeline.ts +4 -8
  51. package/src/lib/toast.ts +134 -0
  52. package/src/lib/upload.ts +71 -0
  53. package/src/lib/url.ts +9 -1
  54. package/src/lib/version.ts +16 -0
  55. package/src/lib/view.ts +9 -10
  56. package/src/middleware/__tests__/auth.test.ts +6 -28
  57. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  58. package/src/middleware/auth.ts +6 -12
  59. package/src/middleware/config.ts +51 -0
  60. package/src/middleware/error-handler.ts +56 -0
  61. package/src/middleware/onboarding.ts +1 -1
  62. package/src/preset.css +6 -0
  63. package/src/routes/__tests__/compose.test.ts +104 -17
  64. package/src/routes/api/__tests__/collections.test.ts +93 -2
  65. package/src/routes/api/__tests__/posts.test.ts +2 -1
  66. package/src/routes/api/__tests__/settings.test.ts +1 -1
  67. package/src/routes/api/collections.ts +64 -68
  68. package/src/routes/api/nav-items.ts +21 -59
  69. package/src/routes/api/pages.ts +18 -46
  70. package/src/routes/api/posts.ts +64 -86
  71. package/src/routes/api/search.ts +6 -4
  72. package/src/routes/api/settings.ts +8 -24
  73. package/src/routes/api/upload.ts +55 -53
  74. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  75. package/src/routes/auth/reset.tsx +17 -66
  76. package/src/routes/auth/setup.tsx +67 -11
  77. package/src/routes/auth/signin.tsx +44 -8
  78. package/src/routes/compose.tsx +194 -0
  79. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  80. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  81. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  82. package/src/routes/dash/appearance.tsx +173 -0
  83. package/src/routes/dash/collections.tsx +80 -14
  84. package/src/routes/dash/index.tsx +12 -14
  85. package/src/routes/dash/media.tsx +46 -49
  86. package/src/routes/dash/pages.tsx +85 -37
  87. package/src/routes/dash/posts.tsx +60 -23
  88. package/src/routes/dash/redirects.tsx +43 -33
  89. package/src/routes/dash/settings.tsx +234 -214
  90. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  91. package/src/routes/feed/rss.ts +11 -16
  92. package/src/routes/feed/sitemap.ts +15 -9
  93. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  94. package/src/routes/pages/archive.tsx +2 -2
  95. package/src/routes/pages/collection.tsx +76 -9
  96. package/src/routes/pages/collections.tsx +3 -1
  97. package/src/routes/pages/featured.tsx +2 -2
  98. package/src/routes/pages/home.tsx +3 -3
  99. package/src/routes/pages/latest.tsx +2 -2
  100. package/src/routes/pages/page.tsx +2 -2
  101. package/src/routes/pages/post.tsx +2 -2
  102. package/src/routes/pages/search.tsx +2 -2
  103. package/src/services/__tests__/collection.test.ts +324 -34
  104. package/src/services/__tests__/media.test.ts +1 -1
  105. package/src/services/__tests__/page.test.ts +116 -1
  106. package/src/services/auth.ts +88 -0
  107. package/src/services/collection.ts +169 -30
  108. package/src/services/index.ts +8 -3
  109. package/src/services/media.ts +39 -12
  110. package/src/services/navigation.ts +17 -5
  111. package/src/services/page.ts +24 -4
  112. package/src/services/post.ts +87 -19
  113. package/src/services/search.ts +0 -1
  114. package/src/services/settings.ts +21 -13
  115. package/src/style.css +3 -0
  116. package/src/styles/components.css +42 -1
  117. package/src/styles/tokens.css +4 -0
  118. package/src/styles/ui.css +902 -73
  119. package/src/types/app-context.ts +25 -0
  120. package/src/types/bindings.ts +1 -0
  121. package/src/types/config.ts +60 -23
  122. package/src/types/entities.ts +12 -2
  123. package/src/types/lingui-react-macro.d.ts +3 -3
  124. package/src/types/operations.ts +2 -4
  125. package/src/types/views.ts +1 -3
  126. package/src/ui/__tests__/font-themes.test.ts +27 -8
  127. package/src/ui/color-themes.ts +1 -1
  128. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  129. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  130. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  131. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  132. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  133. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  134. package/src/ui/components/collection-types.ts +45 -0
  135. package/src/ui/components/compose-types.ts +75 -0
  136. package/src/ui/components/jant-collection-form.ts +512 -0
  137. package/src/ui/components/jant-compose-dialog.ts +494 -0
  138. package/src/ui/components/jant-compose-editor.ts +799 -0
  139. package/src/ui/components/jant-post-form.ts +290 -0
  140. package/src/ui/components/jant-settings-avatar.ts +231 -0
  141. package/src/ui/components/jant-settings-general.ts +436 -0
  142. package/src/ui/components/post-form-template.ts +260 -0
  143. package/src/ui/components/post-form-types.ts +87 -0
  144. package/src/ui/components/settings-types.ts +62 -0
  145. package/src/ui/compose/ComposeDialog.tsx +141 -385
  146. package/src/ui/compose/ComposePrompt.tsx +3 -3
  147. package/src/ui/dash/PostList.tsx +55 -61
  148. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  149. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  150. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  151. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  152. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  153. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  154. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  155. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  156. package/src/ui/dash/index.ts +1 -1
  157. package/src/ui/dash/posts/PostForm.tsx +248 -0
  158. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  159. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  160. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  161. package/src/ui/font-themes.ts +115 -32
  162. package/src/ui/layouts/BaseLayout.tsx +49 -19
  163. package/src/ui/layouts/DashLayout.tsx +14 -9
  164. package/src/ui/layouts/SiteLayout.tsx +38 -23
  165. package/src/ui/pages/CollectionPage.tsx +12 -2
  166. package/src/ui/pages/CollectionsPage.tsx +27 -27
  167. package/src/ui/pages/HomePage.tsx +15 -6
  168. package/src/ui/pages/SearchPage.tsx +1 -2
  169. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  170. package/src/ui/shared/Pagination.tsx +2 -2
  171. package/dist/app.js +0 -265
  172. package/dist/auth.js +0 -36
  173. package/dist/client.js +0 -13
  174. package/dist/db/index.js +0 -10
  175. package/dist/db/schema.js +0 -224
  176. package/dist/i18n/Trans.js +0 -24
  177. package/dist/i18n/context.js +0 -58
  178. package/dist/i18n/detect.js +0 -26
  179. package/dist/i18n/i18n.js +0 -49
  180. package/dist/i18n/index.js +0 -44
  181. package/dist/i18n/locales/en.js +0 -1
  182. package/dist/i18n/locales/zh-Hans.js +0 -1
  183. package/dist/i18n/locales/zh-Hant.js +0 -1
  184. package/dist/i18n/locales.js +0 -13
  185. package/dist/i18n/middleware.js +0 -30
  186. package/dist/lib/avatar-upload.js +0 -134
  187. package/dist/lib/config.js +0 -143
  188. package/dist/lib/constants.js +0 -50
  189. package/dist/lib/excerpt.js +0 -76
  190. package/dist/lib/favicon.js +0 -102
  191. package/dist/lib/feed.js +0 -123
  192. package/dist/lib/image-processor.js +0 -187
  193. package/dist/lib/image.js +0 -97
  194. package/dist/lib/index.js +0 -7
  195. package/dist/lib/markdown.js +0 -83
  196. package/dist/lib/media-helpers.js +0 -49
  197. package/dist/lib/media-upload.js +0 -104
  198. package/dist/lib/nav-reorder.js +0 -27
  199. package/dist/lib/navigation.js +0 -79
  200. package/dist/lib/pagination.js +0 -44
  201. package/dist/lib/render.js +0 -53
  202. package/dist/lib/schemas.js +0 -174
  203. package/dist/lib/sqid.js +0 -72
  204. package/dist/lib/sse.js +0 -218
  205. package/dist/lib/storage.js +0 -164
  206. package/dist/lib/theme.js +0 -65
  207. package/dist/lib/time.js +0 -159
  208. package/dist/lib/timeline.js +0 -95
  209. package/dist/lib/timezones.js +0 -388
  210. package/dist/lib/url.js +0 -89
  211. package/dist/lib/view.js +0 -217
  212. package/dist/middleware/auth.js +0 -52
  213. package/dist/middleware/onboarding.js +0 -41
  214. package/dist/routes/api/collections.js +0 -124
  215. package/dist/routes/api/nav-items.js +0 -104
  216. package/dist/routes/api/pages.js +0 -91
  217. package/dist/routes/api/posts.js +0 -218
  218. package/dist/routes/api/search.js +0 -48
  219. package/dist/routes/api/settings.js +0 -68
  220. package/dist/routes/api/upload.js +0 -246
  221. package/dist/routes/auth/reset.js +0 -221
  222. package/dist/routes/auth/setup.js +0 -194
  223. package/dist/routes/auth/signin.js +0 -176
  224. package/dist/routes/compose.js +0 -48
  225. package/dist/routes/dash/collections.js +0 -115
  226. package/dist/routes/dash/index.js +0 -118
  227. package/dist/routes/dash/media.js +0 -106
  228. package/dist/routes/dash/pages.js +0 -294
  229. package/dist/routes/dash/posts.js +0 -244
  230. package/dist/routes/dash/redirects.js +0 -257
  231. package/dist/routes/dash/settings.js +0 -379
  232. package/dist/routes/feed/rss.js +0 -62
  233. package/dist/routes/feed/sitemap.js +0 -49
  234. package/dist/routes/pages/archive.js +0 -62
  235. package/dist/routes/pages/collection.js +0 -34
  236. package/dist/routes/pages/collections.js +0 -28
  237. package/dist/routes/pages/featured.js +0 -36
  238. package/dist/routes/pages/home.js +0 -64
  239. package/dist/routes/pages/latest.js +0 -45
  240. package/dist/routes/pages/page.js +0 -68
  241. package/dist/routes/pages/post.js +0 -44
  242. package/dist/routes/pages/search.js +0 -54
  243. package/dist/services/collection.js +0 -109
  244. package/dist/services/index.js +0 -24
  245. package/dist/services/media.js +0 -117
  246. package/dist/services/navigation.js +0 -91
  247. package/dist/services/page.js +0 -84
  248. package/dist/services/post.js +0 -229
  249. package/dist/services/redirect.js +0 -48
  250. package/dist/services/search.js +0 -67
  251. package/dist/services/settings.js +0 -68
  252. package/dist/types/bindings.js +0 -3
  253. package/dist/types/config.js +0 -147
  254. package/dist/types/constants.js +0 -27
  255. package/dist/types/entities.js +0 -3
  256. package/dist/types/lingui-react-macro.d.js +0 -9
  257. package/dist/types/operations.js +0 -3
  258. package/dist/types/props.js +0 -3
  259. package/dist/types/sortablejs.d.js +0 -5
  260. package/dist/types/views.js +0 -5
  261. package/dist/types.js +0 -11
  262. package/dist/ui/color-themes.js +0 -268
  263. package/dist/ui/compose/ComposeDialog.js +0 -467
  264. package/dist/ui/compose/ComposePrompt.js +0 -55
  265. package/dist/ui/dash/ActionButtons.js +0 -46
  266. package/dist/ui/dash/CrudPageHeader.js +0 -22
  267. package/dist/ui/dash/DangerZone.js +0 -36
  268. package/dist/ui/dash/FormatBadge.js +0 -27
  269. package/dist/ui/dash/ListItemRow.js +0 -21
  270. package/dist/ui/dash/PageForm.js +0 -195
  271. package/dist/ui/dash/PostForm.js +0 -395
  272. package/dist/ui/dash/PostList.js +0 -83
  273. package/dist/ui/dash/StatusBadge.js +0 -46
  274. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  275. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  276. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  277. package/dist/ui/dash/index.js +0 -10
  278. package/dist/ui/dash/media/MediaListContent.js +0 -166
  279. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  280. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  281. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  282. package/dist/ui/dash/settings/AccountContent.js +0 -209
  283. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  284. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  285. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  286. package/dist/ui/feed/LinkCard.js +0 -72
  287. package/dist/ui/feed/NoteCard.js +0 -58
  288. package/dist/ui/feed/QuoteCard.js +0 -63
  289. package/dist/ui/feed/ThreadPreview.js +0 -48
  290. package/dist/ui/feed/TimelineFeed.js +0 -41
  291. package/dist/ui/feed/TimelineItem.js +0 -27
  292. package/dist/ui/font-themes.js +0 -36
  293. package/dist/ui/layouts/BaseLayout.js +0 -153
  294. package/dist/ui/layouts/DashLayout.js +0 -141
  295. package/dist/ui/layouts/SiteLayout.js +0 -169
  296. package/dist/ui/pages/ArchivePage.js +0 -143
  297. package/dist/ui/pages/CollectionPage.js +0 -70
  298. package/dist/ui/pages/CollectionsPage.js +0 -76
  299. package/dist/ui/pages/FeaturedPage.js +0 -24
  300. package/dist/ui/pages/HomePage.js +0 -24
  301. package/dist/ui/pages/PostPage.js +0 -55
  302. package/dist/ui/pages/SearchPage.js +0 -122
  303. package/dist/ui/pages/SinglePage.js +0 -23
  304. package/dist/ui/shared/EmptyState.js +0 -27
  305. package/dist/ui/shared/MediaGallery.js +0 -35
  306. package/dist/ui/shared/Pagination.js +0 -195
  307. package/dist/ui/shared/ThreadView.js +0 -108
  308. package/dist/ui/shared/index.js +0 -5
  309. package/dist/vendor/datastar.js +0 -1606
  310. package/src/lib/__tests__/config.test.ts +0 -192
  311. package/src/lib/config.ts +0 -167
  312. package/src/routes/compose.ts +0 -63
  313. package/src/ui/dash/PostForm.tsx +0 -360
  314. package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { createTestDatabase } from "../../../__tests__/helpers/db.js";
3
+ import { createPageService } from "../../../services/page.js";
4
+ import { createSettingsService } from "../../../services/settings.js";
5
+ import { createNavItemService } from "../../../services/navigation.js";
6
+ import type { Database } from "../../../db/index.js";
7
+ import type { PageService } from "../../../services/page.js";
8
+ import type { SettingsService } from "../../../services/settings.js";
9
+ import type { NavItemService } from "../../../services/navigation.js";
10
+
11
+ /**
12
+ * Reproduces the seed logic from POST /setup to verify the default About page
13
+ * and navigation items are created correctly.
14
+ */
15
+ async function runSetupSeed(services: {
16
+ pages: PageService;
17
+ settings: SettingsService;
18
+ navItems: NavItemService;
19
+ }) {
20
+ await services.settings.completeOnboarding();
21
+
22
+ await services.navItems.create({
23
+ type: "link",
24
+ label: "Collections",
25
+ url: "/collections",
26
+ });
27
+ await services.navItems.create({
28
+ type: "link",
29
+ label: "Archive",
30
+ url: "/archive",
31
+ });
32
+
33
+ const aboutPage = await services.pages.create({
34
+ slug: "about",
35
+ title: "About",
36
+ body: [
37
+ "Welcome to my corner of the internet.",
38
+ "",
39
+ "This is a place where I share my thoughts, ideas, and things I find interesting. Feel free to look around and get to know what this site is all about.",
40
+ "",
41
+ "If you'd like to get in touch, don't hesitate to reach out.",
42
+ ].join("\n"),
43
+ status: "published",
44
+ });
45
+
46
+ await services.navItems.create({
47
+ type: "page",
48
+ label: "About",
49
+ url: "/about",
50
+ pageId: aboutPage.id,
51
+ });
52
+ }
53
+
54
+ describe("Setup seed logic", () => {
55
+ let services: {
56
+ pages: PageService;
57
+ settings: SettingsService;
58
+ navItems: NavItemService;
59
+ };
60
+
61
+ beforeEach(() => {
62
+ const testDb = createTestDatabase();
63
+ const db = testDb.db as unknown as Database;
64
+ services = {
65
+ pages: createPageService(db),
66
+ settings: createSettingsService(db),
67
+ navItems: createNavItemService(db),
68
+ };
69
+ });
70
+
71
+ it("creates a default About page with correct content", async () => {
72
+ await runSetupSeed(services);
73
+
74
+ const aboutPage = await services.pages.getBySlug("about");
75
+ expect(aboutPage).not.toBeNull();
76
+ expect(aboutPage?.title).toBe("About");
77
+ expect(aboutPage?.status).toBe("published");
78
+ expect(aboutPage?.body).toContain("Welcome to my corner of the internet");
79
+ expect(aboutPage?.bodyHtml).toBeTruthy();
80
+ });
81
+
82
+ it("adds About page to navigation as a page-type nav item", async () => {
83
+ await runSetupSeed(services);
84
+
85
+ const aboutPage = await services.pages.getBySlug("about");
86
+ const navItemsList = await services.navItems.list();
87
+
88
+ const aboutNavItem = navItemsList.find(
89
+ (item) => item.pageId === aboutPage?.id,
90
+ );
91
+ expect(aboutNavItem).toBeDefined();
92
+ expect(aboutNavItem?.type).toBe("page");
93
+ expect(aboutNavItem?.label).toBe("About");
94
+ expect(aboutNavItem?.url).toBe("/about");
95
+ });
96
+
97
+ it("creates three nav items total: Collections, Archive, About", async () => {
98
+ await runSetupSeed(services);
99
+
100
+ const navItemsList = await services.navItems.list();
101
+ expect(navItemsList).toHaveLength(3);
102
+
103
+ const labels = navItemsList.map((item) => item.label);
104
+ expect(labels).toContain("Collections");
105
+ expect(labels).toContain("Archive");
106
+ expect(labels).toContain("About");
107
+ });
108
+
109
+ it("renders About page body as HTML", async () => {
110
+ await runSetupSeed(services);
111
+
112
+ const aboutPage = await services.pages.getBySlug("about");
113
+ expect(aboutPage?.bodyHtml).toContain("<p>");
114
+ expect(aboutPage?.bodyHtml).toContain(
115
+ "Welcome to my corner of the internet",
116
+ );
117
+ });
118
+ });
@@ -6,14 +6,14 @@
6
6
 
7
7
  import { Hono } from "hono";
8
8
  import type { FC } from "hono/jsx";
9
+ import { msg } from "@lingui/core/macro";
9
10
  import { useLingui } from "@lingui/react/macro";
10
- import { hashPassword } from "better-auth/crypto";
11
11
  import type { Bindings } from "../../types.js";
12
- import type { AppVariables } from "../../app.js";
12
+ import type { AppVariables } from "../../types/app-context.js";
13
13
  import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
14
14
  import { dsRedirect, dsToast } from "../../lib/sse.js";
15
- import { SETTINGS_KEYS } from "../../lib/constants.js";
16
15
  import { ResetPasswordSchema } from "../../lib/schemas.js";
16
+ import { getI18n } from "../../i18n/index.js";
17
17
 
18
18
  type Env = { Bindings: Bindings; Variables: AppVariables };
19
19
 
@@ -137,25 +137,6 @@ const ResetErrorContent: FC = () => {
137
137
  );
138
138
  };
139
139
 
140
- /**
141
- * Validate a password reset token against the stored value.
142
- * Returns true if the token is valid and not expired.
143
- */
144
- async function validateResetToken(
145
- settings: { get(key: string): Promise<string | null> },
146
- token: string,
147
- ): Promise<boolean> {
148
- const stored = await settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
149
- if (!stored) return false;
150
-
151
- const separatorIndex = stored.lastIndexOf(":");
152
- const storedToken = stored.substring(0, separatorIndex);
153
- const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
154
- const now = Math.floor(Date.now() / 1000);
155
-
156
- return token === storedToken && now <= expiry;
157
- }
158
-
159
140
  export const resetRoutes = new Hono<Env>();
160
141
 
161
142
  resetRoutes.get("/reset", async (c) => {
@@ -168,7 +149,7 @@ resetRoutes.get("/reset", async (c) => {
168
149
  );
169
150
  }
170
151
 
171
- const isValid = await validateResetToken(c.var.services.settings, token);
152
+ const isValid = await c.var.services.auth.validateResetToken(token);
172
153
  if (!isValid) {
173
154
  return c.html(
174
155
  <BaseLayout title="Reset Password - Jant" c={c}>
@@ -185,55 +166,25 @@ resetRoutes.get("/reset", async (c) => {
185
166
  });
186
167
 
187
168
  resetRoutes.post("/reset", async (c) => {
169
+ const i18n = getI18n(c);
188
170
  const body = await c.req.json();
189
171
  const parsed = ResetPasswordSchema.safeParse(body);
190
172
 
191
173
  if (!parsed.success) {
192
- const msg = parsed.error.errors[0]?.message ?? "Invalid input";
193
- return dsToast(msg, "error");
174
+ const errorMsg =
175
+ parsed.error.issues[0]?.message ??
176
+ i18n._(
177
+ msg({
178
+ message: "Invalid input",
179
+ comment:
180
+ "@context: Fallback validation error for password reset form",
181
+ }),
182
+ );
183
+ return dsToast(errorMsg, "error");
194
184
  }
195
185
 
196
186
  const { password, token } = parsed.data;
197
187
 
198
- // Validate token
199
- const isValid = await validateResetToken(c.var.services.settings, token);
200
- if (!isValid) {
201
- return dsToast("Invalid or expired reset link.", "error");
202
- }
203
-
204
- try {
205
- const hashedPassword = await hashPassword(password);
206
- const db = c.env.DB.withSession() as unknown as D1Database;
207
-
208
- // Get admin user
209
- const userResult = await db
210
- .prepare("SELECT id FROM user LIMIT 1")
211
- .first<{ id: string }>();
212
- if (!userResult) {
213
- return dsToast("No user account found.", "error");
214
- }
215
-
216
- // Update password
217
- await db
218
- .prepare(
219
- "UPDATE account SET password = ? WHERE user_id = ? AND provider_id = 'credential'",
220
- )
221
- .bind(hashedPassword, userResult.id)
222
- .run();
223
-
224
- // Delete all sessions
225
- await db
226
- .prepare("DELETE FROM session WHERE user_id = ?")
227
- .bind(userResult.id)
228
- .run();
229
-
230
- // Delete the reset token
231
- await c.var.services.settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
232
-
233
- return dsRedirect("/signin?reset");
234
- } catch (err) {
235
- // eslint-disable-next-line no-console -- Error logging is intentional
236
- console.error("Password reset error:", err);
237
- return dsToast("Failed to reset password.", "error");
238
- }
188
+ await c.var.services.auth.resetPassword(token, password);
189
+ return dsRedirect("/signin?reset");
239
190
  });
@@ -6,13 +6,15 @@
6
6
 
7
7
  import { Hono } from "hono";
8
8
  import type { FC } from "hono/jsx";
9
+ import { msg } from "@lingui/core/macro";
9
10
  import { useLingui } from "@lingui/react/macro";
10
11
  import type { Bindings } from "../../types.js";
11
- import type { AppVariables } from "../../app.js";
12
+ import type { AppVariables } from "../../types/app-context.js";
12
13
  import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
13
14
  import { dsRedirect, dsToast } from "../../lib/sse.js";
14
15
  import { SetupSchema } from "../../lib/schemas.js";
15
16
  import { mapIanaToTimezone } from "../../lib/timezones.js";
17
+ import { getI18n } from "../../i18n/index.js";
16
18
 
17
19
  type Env = { Bindings: Bindings; Variables: AppVariables };
18
20
 
@@ -131,6 +133,7 @@ setupRoutes.get("/setup", async (c) => {
131
133
  });
132
134
 
133
135
  setupRoutes.post("/setup", async (c) => {
136
+ const i18n = getI18n(c);
134
137
  const isComplete = await c.var.services.settings.isOnboardingComplete();
135
138
  if (isComplete) return c.redirect("/");
136
139
 
@@ -139,14 +142,30 @@ setupRoutes.post("/setup", async (c) => {
139
142
  const browserTimezone = body._timezone;
140
143
 
141
144
  if (!parsed.success) {
142
- const msg = parsed.error.errors[0]?.message ?? "Invalid input";
143
- return dsToast(msg, "error");
145
+ const errorMsg =
146
+ parsed.error.issues[0]?.message ??
147
+ i18n._(
148
+ msg({
149
+ message: "Invalid input",
150
+ comment: "@context: Fallback validation error for setup form",
151
+ }),
152
+ );
153
+ return dsToast(errorMsg, "error");
144
154
  }
145
155
 
146
156
  const { name, email, password } = parsed.data;
147
157
 
148
158
  if (!c.var.auth) {
149
- return dsToast("AUTH_SECRET not configured", "error");
159
+ return dsToast(
160
+ i18n._(
161
+ msg({
162
+ message: "AUTH_SECRET not configured",
163
+ comment:
164
+ "@context: Error toast when authentication secret is missing from server config",
165
+ }),
166
+ ),
167
+ "error",
168
+ );
150
169
  }
151
170
 
152
171
  try {
@@ -155,7 +174,15 @@ setupRoutes.post("/setup", async (c) => {
155
174
  });
156
175
 
157
176
  if (!signUpResponse || "error" in signUpResponse) {
158
- return dsToast("Failed to create account", "error");
177
+ return dsToast(
178
+ i18n._(
179
+ msg({
180
+ message: "Failed to create account",
181
+ comment: "@context: Error toast when account creation fails",
182
+ }),
183
+ ),
184
+ "error",
185
+ );
159
186
  }
160
187
 
161
188
  await c.var.services.settings.completeOnboarding();
@@ -168,22 +195,51 @@ setupRoutes.post("/setup", async (c) => {
168
195
  }
169
196
  }
170
197
 
171
- // Seed default navigation items
172
198
  await c.var.services.navItems.create({
173
199
  type: "link",
174
- label: "Featured",
175
- url: "/featured",
200
+ label: "Collections",
201
+ url: "/collections",
176
202
  });
203
+ // Seed default navigation items
177
204
  await c.var.services.navItems.create({
178
205
  type: "link",
179
- label: "Collections",
180
- url: "/collections",
206
+ label: "Archive",
207
+ url: "/archive",
208
+ });
209
+
210
+ // Seed default About page
211
+ const aboutPage = await c.var.services.pages.create({
212
+ slug: "about",
213
+ title: "About",
214
+ body: [
215
+ "Welcome to my corner of the internet.",
216
+ "",
217
+ "This is a place where I share my thoughts, ideas, and things I find interesting. Feel free to look around and get to know what this site is all about.",
218
+ "",
219
+ "If you'd like to get in touch, don't hesitate to reach out.",
220
+ ].join("\n"),
221
+ status: "published",
222
+ });
223
+
224
+ await c.var.services.navItems.create({
225
+ type: "page",
226
+ label: "About",
227
+ url: "/about",
228
+ pageId: aboutPage.id,
181
229
  });
182
230
 
183
231
  return dsRedirect("/signin?setup");
184
232
  } catch (err) {
185
233
  // eslint-disable-next-line no-console -- Error logging is intentional
186
234
  console.error("Setup error:", err);
187
- return dsToast("Failed to create account", "error");
235
+ return dsToast(
236
+ i18n._(
237
+ msg({
238
+ message: "Failed to create account",
239
+ comment: "@context: Error toast when account creation fails",
240
+ }),
241
+ ),
242
+ "error",
243
+ );
188
244
  }
189
245
  });
@@ -4,12 +4,14 @@
4
4
 
5
5
  import { Hono } from "hono";
6
6
  import type { FC } from "hono/jsx";
7
+ import { msg } from "@lingui/core/macro";
7
8
  import { useLingui } from "@lingui/react/macro";
8
9
  import type { Bindings } from "../../types.js";
9
- import type { AppVariables } from "../../app.js";
10
+ import type { AppVariables } from "../../types/app-context.js";
10
11
  import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
11
12
  import { dsRedirect, dsToast } from "../../lib/sse.js";
12
13
  import { SigninSchema } from "../../lib/schemas.js";
14
+ import { getI18n } from "../../i18n/index.js";
13
15
 
14
16
  type Env = { Bindings: Bindings; Variables: AppVariables };
15
17
 
@@ -116,24 +118,42 @@ signinRoutes.get("/signin", async (c) => {
116
118
  return c.html(
117
119
  <BaseLayout title="Sign In - Jant" c={c} toast={toast}>
118
120
  <SigninContent
119
- demoEmail={c.env.DEMO_EMAIL}
120
- demoPassword={c.env.DEMO_PASSWORD}
121
+ demoEmail={c.var.appConfig.demoEmail}
122
+ demoPassword={c.var.appConfig.demoPassword}
121
123
  />
122
124
  </BaseLayout>,
123
125
  );
124
126
  });
125
127
 
126
128
  signinRoutes.post("/signin", async (c) => {
129
+ const i18n = getI18n(c);
130
+
127
131
  if (!c.var.auth) {
128
- return dsToast("Auth not configured", "error");
132
+ return dsToast(
133
+ i18n._(
134
+ msg({
135
+ message: "Auth not configured",
136
+ comment:
137
+ "@context: Error toast when authentication system is unavailable",
138
+ }),
139
+ ),
140
+ "error",
141
+ );
129
142
  }
130
143
 
131
144
  const body = await c.req.json();
132
145
  const parsed = SigninSchema.safeParse(body);
133
146
 
134
147
  if (!parsed.success) {
135
- const msg = parsed.error.errors[0]?.message ?? "Invalid input";
136
- return dsToast(msg, "error");
148
+ const errorMsg =
149
+ parsed.error.issues[0]?.message ??
150
+ i18n._(
151
+ msg({
152
+ message: "Invalid input",
153
+ comment: "@context: Fallback validation error for sign-in form",
154
+ }),
155
+ );
156
+ return dsToast(errorMsg, "error");
137
157
  }
138
158
 
139
159
  const { email, password } = parsed.data;
@@ -147,14 +167,30 @@ signinRoutes.post("/signin", async (c) => {
147
167
 
148
168
  return dsRedirect("/dash", { headers });
149
169
  } catch {
150
- return dsToast("Invalid email or password", "error");
170
+ return dsToast(
171
+ i18n._(
172
+ msg({
173
+ message: "Invalid email or password",
174
+ comment: "@context: Error toast when sign-in credentials are wrong",
175
+ }),
176
+ ),
177
+ "error",
178
+ );
151
179
  }
152
180
  });
153
181
 
154
182
  signinRoutes.get("/signout", async (c) => {
155
183
  if (c.var.auth) {
156
184
  try {
157
- await c.var.auth.api.signOut({ headers: c.req.raw.headers });
185
+ const res = await c.var.auth.api.signOut({
186
+ headers: c.req.raw.headers,
187
+ asResponse: true,
188
+ });
189
+ const redirect = c.redirect("/");
190
+ for (const cookie of res.headers.getSetCookie()) {
191
+ redirect.headers.append("Set-Cookie", cookie);
192
+ }
193
+ return redirect;
158
194
  } catch {
159
195
  // Ignore signout errors
160
196
  }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Compose Route
3
+ *
4
+ * Handles post creation from the public-site compose dialog.
5
+ * Published posts are prepended to the homepage timeline via SSE.
6
+ * Drafts close the dialog and show a confirmation toast.
7
+ */
8
+
9
+ import { Hono, type Context } from "hono";
10
+ import { msg } from "@lingui/core/macro";
11
+ import type { Bindings, Post } from "../types.js";
12
+ import type { AppVariables } from "../types/app-context.js";
13
+ import { requireAuth } from "../middleware/auth.js";
14
+ import { CreatePostSchema, validateMediaCount } from "../lib/schemas.js";
15
+ import { sse, dsToast } from "../lib/sse.js";
16
+ import { getI18n } from "../i18n/index.js";
17
+ import {
18
+ toPostView,
19
+ toPostViewFromPost,
20
+ createMediaContext,
21
+ } from "../lib/view.js";
22
+ import { buildMediaMap } from "../lib/media-helpers.js";
23
+ import { TimelineItemFromPost } from "../ui/feed/TimelineItem.js";
24
+
25
+ type Env = { Bindings: Bindings; Variables: AppVariables };
26
+
27
+ export const composeRoutes = new Hono<Env>();
28
+
29
+ // All compose routes require authentication
30
+ composeRoutes.use("*", requireAuth());
31
+
32
+ /** Reset compose form signals to initial values */
33
+ const INITIAL_SIGNALS = {
34
+ format: "note",
35
+ title: "",
36
+ body: "",
37
+ url: "",
38
+ quoteText: "",
39
+ status: "published",
40
+ rating: 0,
41
+ collectionIds: [],
42
+ mediaIds: [],
43
+ _composeLoading: false,
44
+ _showRating: false,
45
+ _showCollection: false,
46
+ };
47
+
48
+ /** Script fragment that closes the compose dialog and self-removes */
49
+ const CLOSE_DIALOG_SCRIPT =
50
+ "<script data-effect=\"el.remove()\">document.getElementById('compose-dialog').close()</script>";
51
+
52
+ /** Build a timeline card HTML string for a newly created post */
53
+ async function buildTimelineCard(
54
+ c: Context<Env>,
55
+ post: Post,
56
+ mediaIds: string[] | undefined,
57
+ ): Promise<string> {
58
+ const mediaCtx = createMediaContext(c.var.appConfig);
59
+ let postView;
60
+
61
+ if (mediaIds && mediaIds.length > 0) {
62
+ const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
63
+ const mediaMap = buildMediaMap(
64
+ rawMediaMap,
65
+ mediaCtx.r2PublicUrl,
66
+ mediaCtx.imageTransformUrl,
67
+ mediaCtx.s3PublicUrl,
68
+ );
69
+ postView = toPostView(
70
+ { ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
71
+ mediaCtx,
72
+ );
73
+ } else {
74
+ postView = toPostViewFromPost(post, mediaCtx);
75
+ }
76
+
77
+ return (
78
+ <div>
79
+ <TimelineItemFromPost post={postView} />
80
+ <hr class="feed-divider" />
81
+ </div>
82
+ ).toString();
83
+ }
84
+
85
+ composeRoutes.post("/", async (c) => {
86
+ const i18n = getI18n(c);
87
+ const raw = await c.req.json();
88
+ const wantsJson = c.req.header("accept")?.includes("application/json");
89
+
90
+ const result = CreatePostSchema.safeParse(raw);
91
+ if (!result.success) {
92
+ const firstError =
93
+ result.error.issues[0]?.message ??
94
+ i18n._(
95
+ msg({
96
+ message: "Invalid input",
97
+ comment: "@context: Fallback validation error for compose form",
98
+ }),
99
+ );
100
+ if (wantsJson) {
101
+ return c.json({ status: "error" as const, error: firstError }, 422);
102
+ }
103
+ return dsToast(firstError, "error");
104
+ }
105
+
106
+ const data = result.data;
107
+
108
+ // Validate media count
109
+ if (data.mediaIds) {
110
+ const mediaError = validateMediaCount(data.mediaIds);
111
+ if (mediaError) {
112
+ return dsToast(mediaError, "error");
113
+ }
114
+ }
115
+
116
+ const post = await c.var.services.posts.create({
117
+ format: data.format,
118
+ title: data.title || undefined,
119
+ body: data.body || undefined,
120
+ status: data.status ?? "published",
121
+ url: data.url || undefined,
122
+ quoteText: data.quoteText || undefined,
123
+ rating: data.rating || undefined,
124
+ collectionIds: data.collectionIds?.length ? data.collectionIds : undefined,
125
+ });
126
+
127
+ // Attach media if provided
128
+ if (data.mediaIds && data.mediaIds.length > 0) {
129
+ await c.var.services.media.attachToPost(post.id, data.mediaIds);
130
+
131
+ // Save alt text for each media item
132
+ if (data.mediaAlts) {
133
+ const altEntries = Object.entries(data.mediaAlts).filter(
134
+ ([id, alt]) => alt && (data.mediaIds ?? []).includes(id),
135
+ );
136
+ await Promise.all(
137
+ altEntries.map(([id, alt]) => c.var.services.media.updateAlt(id, alt)),
138
+ );
139
+ }
140
+ }
141
+
142
+ const isDraft = (data.status ?? "published") === "draft";
143
+
144
+ // ── JSON response mode (used by Lit compose bridge) ──────────────
145
+ if (wantsJson) {
146
+ if (isDraft) {
147
+ return c.json({
148
+ status: "draft" as const,
149
+ toast: i18n._(
150
+ msg({
151
+ message: "Draft saved.",
152
+ comment: "@context: Toast after saving a draft post",
153
+ }),
154
+ ),
155
+ });
156
+ }
157
+
158
+ const cardHtml = await buildTimelineCard(c, post, data.mediaIds);
159
+ return c.json({ status: "published" as const, cardHtml });
160
+ }
161
+
162
+ // ── SSE response mode (used by Datastar) ─────────────────────────
163
+ if (isDraft) {
164
+ return sse(c, async (stream) => {
165
+ await stream.patchElements(CLOSE_DIALOG_SCRIPT, {
166
+ mode: "append",
167
+ selector: "body",
168
+ });
169
+ await stream.patchSignals(INITIAL_SIGNALS);
170
+ await stream.toast(
171
+ i18n._(
172
+ msg({
173
+ message: "Draft saved.",
174
+ comment: "@context: Toast after saving a draft post",
175
+ }),
176
+ ),
177
+ );
178
+ });
179
+ }
180
+
181
+ const cardHtml = await buildTimelineCard(c, post, data.mediaIds);
182
+
183
+ return sse(c, async (stream) => {
184
+ await stream.patchElements(cardHtml, {
185
+ mode: "prepend",
186
+ selector: "#timeline-items",
187
+ });
188
+ await stream.patchElements(CLOSE_DIALOG_SCRIPT, {
189
+ mode: "append",
190
+ selector: "body",
191
+ });
192
+ await stream.patchSignals(INITIAL_SIGNALS);
193
+ });
194
+ });