@jant/core 0.3.27 → 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 (313) 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 +111 -174
  9. package/src/client.ts +13 -0
  10. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  11. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  12. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  13. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  14. package/src/db/schema.ts +24 -4
  15. package/src/i18n/locales/en.po +810 -385
  16. package/src/i18n/locales/en.ts +1 -1
  17. package/src/i18n/locales/zh-Hans.po +733 -522
  18. package/src/i18n/locales/zh-Hans.ts +1 -1
  19. package/src/i18n/locales/zh-Hant.po +733 -522
  20. package/src/i18n/locales/zh-Hant.ts +1 -1
  21. package/src/i18n/middleware.ts +7 -11
  22. package/src/index.ts +1 -1
  23. package/src/lib/__tests__/icons.test.ts +178 -0
  24. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  25. package/src/lib/__tests__/schemas.test.ts +12 -6
  26. package/src/lib/__tests__/theme.test.ts +62 -0
  27. package/src/lib/__tests__/timezones.test.ts +1 -1
  28. package/src/lib/__tests__/url.test.ts +12 -0
  29. package/src/lib/__tests__/view.test.ts +1 -5
  30. package/src/lib/avatar-upload.ts +18 -10
  31. package/src/lib/collection-form-bridge.ts +52 -0
  32. package/src/lib/collections-reorder.ts +28 -0
  33. package/src/lib/compose-bridge.ts +251 -0
  34. package/src/lib/errors.ts +116 -0
  35. package/src/lib/excerpt.ts +1 -1
  36. package/src/lib/favicon.ts +3 -5
  37. package/src/lib/html.ts +22 -0
  38. package/src/lib/icon-catalog.ts +181 -0
  39. package/src/lib/icons.ts +202 -0
  40. package/src/lib/navigation.ts +18 -33
  41. package/src/lib/pagination.ts +3 -2
  42. package/src/lib/post-form-bridge.ts +136 -0
  43. package/src/lib/render.tsx +11 -4
  44. package/src/lib/resolve-config.ts +157 -0
  45. package/src/lib/schemas.ts +76 -12
  46. package/src/lib/settings-bridge.ts +139 -0
  47. package/src/lib/storage.ts +37 -16
  48. package/src/lib/theme.ts +5 -7
  49. package/src/lib/timeline.ts +4 -8
  50. package/src/lib/toast.ts +134 -0
  51. package/src/lib/upload.ts +71 -0
  52. package/src/lib/url.ts +9 -1
  53. package/src/lib/version.ts +16 -0
  54. package/src/lib/view.ts +9 -10
  55. package/src/middleware/__tests__/auth.test.ts +6 -28
  56. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  57. package/src/middleware/auth.ts +6 -12
  58. package/src/middleware/config.ts +51 -0
  59. package/src/middleware/error-handler.ts +56 -0
  60. package/src/middleware/onboarding.ts +1 -1
  61. package/src/preset.css +6 -0
  62. package/src/routes/__tests__/compose.test.ts +104 -17
  63. package/src/routes/api/__tests__/collections.test.ts +93 -2
  64. package/src/routes/api/__tests__/posts.test.ts +2 -1
  65. package/src/routes/api/__tests__/settings.test.ts +1 -1
  66. package/src/routes/api/collections.ts +64 -68
  67. package/src/routes/api/nav-items.ts +21 -59
  68. package/src/routes/api/pages.ts +18 -46
  69. package/src/routes/api/posts.ts +64 -86
  70. package/src/routes/api/search.ts +6 -4
  71. package/src/routes/api/settings.ts +8 -24
  72. package/src/routes/api/upload.ts +55 -53
  73. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  74. package/src/routes/auth/reset.tsx +17 -66
  75. package/src/routes/auth/setup.tsx +67 -11
  76. package/src/routes/auth/signin.tsx +44 -8
  77. package/src/routes/compose.tsx +194 -0
  78. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  79. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  80. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  81. package/src/routes/dash/appearance.tsx +173 -0
  82. package/src/routes/dash/collections.tsx +80 -14
  83. package/src/routes/dash/index.tsx +12 -14
  84. package/src/routes/dash/media.tsx +46 -49
  85. package/src/routes/dash/pages.tsx +85 -37
  86. package/src/routes/dash/posts.tsx +60 -23
  87. package/src/routes/dash/redirects.tsx +43 -33
  88. package/src/routes/dash/settings.tsx +234 -214
  89. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  90. package/src/routes/feed/rss.ts +11 -16
  91. package/src/routes/feed/sitemap.ts +15 -9
  92. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  93. package/src/routes/pages/archive.tsx +2 -2
  94. package/src/routes/pages/collection.tsx +76 -9
  95. package/src/routes/pages/collections.tsx +3 -1
  96. package/src/routes/pages/featured.tsx +2 -2
  97. package/src/routes/pages/home.tsx +3 -3
  98. package/src/routes/pages/latest.tsx +2 -2
  99. package/src/routes/pages/page.tsx +2 -2
  100. package/src/routes/pages/post.tsx +2 -2
  101. package/src/routes/pages/search.tsx +2 -2
  102. package/src/services/__tests__/collection.test.ts +324 -34
  103. package/src/services/__tests__/media.test.ts +1 -1
  104. package/src/services/__tests__/page.test.ts +116 -1
  105. package/src/services/auth.ts +88 -0
  106. package/src/services/collection.ts +169 -30
  107. package/src/services/index.ts +8 -3
  108. package/src/services/media.ts +39 -12
  109. package/src/services/navigation.ts +17 -5
  110. package/src/services/page.ts +24 -4
  111. package/src/services/post.ts +87 -19
  112. package/src/services/search.ts +0 -1
  113. package/src/services/settings.ts +21 -13
  114. package/src/style.css +3 -0
  115. package/src/styles/components.css +42 -1
  116. package/src/styles/tokens.css +4 -0
  117. package/src/styles/ui.css +902 -73
  118. package/src/types/app-context.ts +25 -0
  119. package/src/types/bindings.ts +1 -0
  120. package/src/types/config.ts +60 -23
  121. package/src/types/entities.ts +12 -2
  122. package/src/types/lingui-react-macro.d.ts +3 -3
  123. package/src/types/operations.ts +2 -4
  124. package/src/types/views.ts +1 -3
  125. package/src/ui/__tests__/font-themes.test.ts +27 -8
  126. package/src/ui/color-themes.ts +1 -1
  127. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  128. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  129. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  130. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  131. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  132. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  133. package/src/ui/components/collection-types.ts +45 -0
  134. package/src/ui/components/compose-types.ts +75 -0
  135. package/src/ui/components/jant-collection-form.ts +512 -0
  136. package/src/ui/components/jant-compose-dialog.ts +494 -0
  137. package/src/ui/components/jant-compose-editor.ts +799 -0
  138. package/src/ui/components/jant-post-form.ts +290 -0
  139. package/src/ui/components/jant-settings-avatar.ts +231 -0
  140. package/src/ui/components/jant-settings-general.ts +436 -0
  141. package/src/ui/components/post-form-template.ts +260 -0
  142. package/src/ui/components/post-form-types.ts +87 -0
  143. package/src/ui/components/settings-types.ts +62 -0
  144. package/src/ui/compose/ComposeDialog.tsx +141 -385
  145. package/src/ui/compose/ComposePrompt.tsx +3 -3
  146. package/src/ui/dash/PostList.tsx +55 -61
  147. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  148. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  149. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  150. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  151. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  152. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  153. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  154. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  155. package/src/ui/dash/index.ts +1 -1
  156. package/src/ui/dash/posts/PostForm.tsx +248 -0
  157. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  158. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  159. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  160. package/src/ui/font-themes.ts +115 -32
  161. package/src/ui/layouts/BaseLayout.tsx +49 -19
  162. package/src/ui/layouts/DashLayout.tsx +14 -9
  163. package/src/ui/layouts/SiteLayout.tsx +38 -23
  164. package/src/ui/pages/CollectionPage.tsx +12 -2
  165. package/src/ui/pages/CollectionsPage.tsx +27 -27
  166. package/src/ui/pages/HomePage.tsx +15 -6
  167. package/src/ui/pages/SearchPage.tsx +1 -2
  168. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  169. package/src/ui/shared/Pagination.tsx +2 -2
  170. package/dist/app.js +0 -267
  171. package/dist/auth.js +0 -39
  172. package/dist/client.js +0 -13
  173. package/dist/db/index.js +0 -10
  174. package/dist/db/schema.js +0 -224
  175. package/dist/i18n/Trans.js +0 -24
  176. package/dist/i18n/context.js +0 -58
  177. package/dist/i18n/detect.js +0 -26
  178. package/dist/i18n/i18n.js +0 -49
  179. package/dist/i18n/index.js +0 -44
  180. package/dist/i18n/locales/en.js +0 -1
  181. package/dist/i18n/locales/zh-Hans.js +0 -1
  182. package/dist/i18n/locales/zh-Hant.js +0 -1
  183. package/dist/i18n/locales.js +0 -13
  184. package/dist/i18n/middleware.js +0 -30
  185. package/dist/lib/avatar-upload.js +0 -134
  186. package/dist/lib/config.js +0 -143
  187. package/dist/lib/constants.js +0 -50
  188. package/dist/lib/excerpt.js +0 -76
  189. package/dist/lib/favicon.js +0 -102
  190. package/dist/lib/feed.js +0 -123
  191. package/dist/lib/image-processor.js +0 -187
  192. package/dist/lib/image.js +0 -97
  193. package/dist/lib/index.js +0 -7
  194. package/dist/lib/markdown.js +0 -83
  195. package/dist/lib/media-helpers.js +0 -49
  196. package/dist/lib/media-upload.js +0 -104
  197. package/dist/lib/nav-reorder.js +0 -27
  198. package/dist/lib/navigation.js +0 -79
  199. package/dist/lib/pagination.js +0 -44
  200. package/dist/lib/render.js +0 -53
  201. package/dist/lib/schemas.js +0 -174
  202. package/dist/lib/sqid.js +0 -72
  203. package/dist/lib/sse.js +0 -218
  204. package/dist/lib/storage.js +0 -164
  205. package/dist/lib/theme.js +0 -65
  206. package/dist/lib/time.js +0 -159
  207. package/dist/lib/timeline.js +0 -95
  208. package/dist/lib/timezones.js +0 -388
  209. package/dist/lib/url.js +0 -89
  210. package/dist/lib/view.js +0 -217
  211. package/dist/middleware/auth.js +0 -52
  212. package/dist/middleware/onboarding.js +0 -41
  213. package/dist/routes/api/collections.js +0 -124
  214. package/dist/routes/api/nav-items.js +0 -104
  215. package/dist/routes/api/pages.js +0 -91
  216. package/dist/routes/api/posts.js +0 -218
  217. package/dist/routes/api/search.js +0 -48
  218. package/dist/routes/api/settings.js +0 -68
  219. package/dist/routes/api/upload.js +0 -246
  220. package/dist/routes/auth/reset.js +0 -221
  221. package/dist/routes/auth/setup.js +0 -194
  222. package/dist/routes/auth/signin.js +0 -176
  223. package/dist/routes/compose.js +0 -48
  224. package/dist/routes/dash/collections.js +0 -115
  225. package/dist/routes/dash/index.js +0 -118
  226. package/dist/routes/dash/media.js +0 -106
  227. package/dist/routes/dash/pages.js +0 -294
  228. package/dist/routes/dash/posts.js +0 -244
  229. package/dist/routes/dash/redirects.js +0 -257
  230. package/dist/routes/dash/settings.js +0 -379
  231. package/dist/routes/feed/rss.js +0 -62
  232. package/dist/routes/feed/sitemap.js +0 -49
  233. package/dist/routes/pages/archive.js +0 -62
  234. package/dist/routes/pages/collection.js +0 -34
  235. package/dist/routes/pages/collections.js +0 -28
  236. package/dist/routes/pages/featured.js +0 -36
  237. package/dist/routes/pages/home.js +0 -64
  238. package/dist/routes/pages/latest.js +0 -45
  239. package/dist/routes/pages/page.js +0 -68
  240. package/dist/routes/pages/post.js +0 -44
  241. package/dist/routes/pages/search.js +0 -54
  242. package/dist/services/collection.js +0 -109
  243. package/dist/services/index.js +0 -24
  244. package/dist/services/media.js +0 -117
  245. package/dist/services/navigation.js +0 -91
  246. package/dist/services/page.js +0 -84
  247. package/dist/services/post.js +0 -229
  248. package/dist/services/redirect.js +0 -48
  249. package/dist/services/search.js +0 -67
  250. package/dist/services/settings.js +0 -68
  251. package/dist/types/bindings.js +0 -3
  252. package/dist/types/config.js +0 -147
  253. package/dist/types/constants.js +0 -27
  254. package/dist/types/entities.js +0 -3
  255. package/dist/types/lingui-react-macro.d.js +0 -9
  256. package/dist/types/operations.js +0 -3
  257. package/dist/types/props.js +0 -3
  258. package/dist/types/sortablejs.d.js +0 -5
  259. package/dist/types/views.js +0 -5
  260. package/dist/types.js +0 -11
  261. package/dist/ui/color-themes.js +0 -268
  262. package/dist/ui/compose/ComposeDialog.js +0 -467
  263. package/dist/ui/compose/ComposePrompt.js +0 -55
  264. package/dist/ui/dash/ActionButtons.js +0 -46
  265. package/dist/ui/dash/CrudPageHeader.js +0 -22
  266. package/dist/ui/dash/DangerZone.js +0 -36
  267. package/dist/ui/dash/FormatBadge.js +0 -27
  268. package/dist/ui/dash/ListItemRow.js +0 -21
  269. package/dist/ui/dash/PageForm.js +0 -195
  270. package/dist/ui/dash/PostForm.js +0 -395
  271. package/dist/ui/dash/PostList.js +0 -83
  272. package/dist/ui/dash/StatusBadge.js +0 -46
  273. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  274. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  275. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  276. package/dist/ui/dash/index.js +0 -10
  277. package/dist/ui/dash/media/MediaListContent.js +0 -166
  278. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  279. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  280. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  281. package/dist/ui/dash/settings/AccountContent.js +0 -209
  282. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  283. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  284. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  285. package/dist/ui/feed/LinkCard.js +0 -72
  286. package/dist/ui/feed/NoteCard.js +0 -58
  287. package/dist/ui/feed/QuoteCard.js +0 -63
  288. package/dist/ui/feed/ThreadPreview.js +0 -48
  289. package/dist/ui/feed/TimelineFeed.js +0 -41
  290. package/dist/ui/feed/TimelineItem.js +0 -27
  291. package/dist/ui/font-themes.js +0 -36
  292. package/dist/ui/layouts/BaseLayout.js +0 -153
  293. package/dist/ui/layouts/DashLayout.js +0 -141
  294. package/dist/ui/layouts/SiteLayout.js +0 -169
  295. package/dist/ui/pages/ArchivePage.js +0 -143
  296. package/dist/ui/pages/CollectionPage.js +0 -70
  297. package/dist/ui/pages/CollectionsPage.js +0 -76
  298. package/dist/ui/pages/FeaturedPage.js +0 -24
  299. package/dist/ui/pages/HomePage.js +0 -24
  300. package/dist/ui/pages/PostPage.js +0 -55
  301. package/dist/ui/pages/SearchPage.js +0 -122
  302. package/dist/ui/pages/SinglePage.js +0 -23
  303. package/dist/ui/shared/EmptyState.js +0 -27
  304. package/dist/ui/shared/MediaGallery.js +0 -35
  305. package/dist/ui/shared/Pagination.js +0 -195
  306. package/dist/ui/shared/ThreadView.js +0 -108
  307. package/dist/ui/shared/index.js +0 -5
  308. package/dist/vendor/datastar.js +0 -1606
  309. package/src/lib/__tests__/config.test.ts +0 -192
  310. package/src/lib/config.ts +0 -167
  311. package/src/routes/compose.ts +0 -63
  312. package/src/ui/dash/PostForm.tsx +0 -360
  313. 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
+ });