@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
@@ -1,379 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Dashboard Settings Routes
4
- *
5
- * Sub-pages: General, Appearance, Account
6
- */ import { Hono } from "hono";
7
- import { DashLayout } from "../../ui/layouts/DashLayout.js";
8
- import { sse, dsRedirect, dsToast } from "../../lib/sse.js";
9
- import { arrayBufferToBase64 } from "../../lib/favicon.js";
10
- import { getSiteLanguage, getSiteName, getHomeDefaultView, getTimeZone, getSiteFooter, isNoIndex, getConfigFallback } from "../../lib/config.js";
11
- import { SETTINGS_KEYS } from "../../lib/constants.js";
12
- import { getAvailableThemes } from "../../lib/theme.js";
13
- import { getMediaUrl, getPublicUrlForProvider } from "../../lib/image.js";
14
- import { TIMEZONES } from "../../lib/timezones.js";
15
- import { BUILTIN_FONT_THEMES } from "../../ui/font-themes.js";
16
- import { GeneralContent } from "../../ui/dash/settings/GeneralContent.js";
17
- import { AppearanceContent } from "../../ui/dash/settings/AppearanceContent.js";
18
- import { AccountContent } from "../../ui/dash/settings/AccountContent.js";
19
- /** Escape HTML special characters for safe insertion into HTML strings */ function escapeHtml(str) {
20
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
21
- }
22
- export const settingsRoutes = new Hono();
23
- // ===========================================================================
24
- // General settings
25
- // ===========================================================================
26
- /** Resolve the avatar storage key to a URL */ async function resolveAvatarUrl(c) {
27
- const avatarKey = await c.var.services.settings.get("SITE_AVATAR");
28
- if (!avatarKey) return "";
29
- const publicUrl = getPublicUrlForProvider(c.env.STORAGE_DRIVER || "r2", c.env.R2_PUBLIC_URL, c.env.S3_PUBLIC_URL);
30
- return getMediaUrl(avatarKey, publicUrl);
31
- }
32
- settingsRoutes.get("/", async (c)=>{
33
- const { settings } = c.var.services;
34
- const dbSiteName = await settings.get("SITE_NAME");
35
- const dbSiteDescription = await settings.get("SITE_DESCRIPTION");
36
- const [siteLanguage, homeDefaultView, timeZone, siteFooter, noindex] = await Promise.all([
37
- getSiteLanguage(c),
38
- getHomeDefaultView(c),
39
- getTimeZone(c),
40
- getSiteFooter(c),
41
- isNoIndex(c)
42
- ]);
43
- const siteNameFallback = getConfigFallback(c, "SITE_NAME");
44
- const siteDescriptionFallback = getConfigFallback(c, "SITE_DESCRIPTION");
45
- const siteAvatarUrl = await resolveAvatarUrl(c);
46
- const showHeaderAvatar = await settings.get("SHOW_HEADER_AVATAR") === "true";
47
- const saved = c.req.query("saved") !== undefined;
48
- return c.html(/*#__PURE__*/ _jsx(DashLayout, {
49
- c: c,
50
- title: "Settings",
51
- siteName: dbSiteName || siteNameFallback,
52
- currentPath: "/dash/settings",
53
- toast: saved ? {
54
- message: "Settings saved successfully."
55
- } : undefined,
56
- children: /*#__PURE__*/ _jsx(GeneralContent, {
57
- siteName: dbSiteName || "",
58
- siteDescription: dbSiteDescription || "",
59
- siteLanguage: siteLanguage,
60
- homeDefaultView: homeDefaultView,
61
- siteNameFallback: siteNameFallback,
62
- siteDescriptionFallback: siteDescriptionFallback,
63
- siteAvatarUrl: siteAvatarUrl,
64
- showHeaderAvatar: showHeaderAvatar,
65
- timeZone: timeZone,
66
- siteFooter: siteFooter,
67
- noindex: noindex,
68
- timezones: TIMEZONES
69
- })
70
- }));
71
- });
72
- settingsRoutes.post("/", async (c)=>{
73
- const body = await c.req.json();
74
- const { settings } = c.var.services;
75
- const oldLanguage = await settings.get("SITE_LANGUAGE") ?? "en";
76
- if (body.siteName.trim()) {
77
- await settings.set("SITE_NAME", body.siteName.trim());
78
- } else {
79
- await settings.remove("SITE_NAME");
80
- }
81
- if (body.siteDescription.trim()) {
82
- await settings.set("SITE_DESCRIPTION", body.siteDescription.trim());
83
- } else {
84
- await settings.remove("SITE_DESCRIPTION");
85
- }
86
- await settings.set("SITE_LANGUAGE", body.siteLanguage);
87
- // Save homepage default view (only store if non-default)
88
- if (body.homeDefaultView === "featured") {
89
- await settings.set("HOME_DEFAULT_VIEW", body.homeDefaultView);
90
- } else {
91
- await settings.remove("HOME_DEFAULT_VIEW");
92
- }
93
- // Timezone
94
- if (body.timeZone && body.timeZone !== "UTC") {
95
- await settings.set("TIME_ZONE", body.timeZone);
96
- } else {
97
- await settings.remove("TIME_ZONE");
98
- }
99
- const languageChanged = oldLanguage !== body.siteLanguage;
100
- const displayName = body.siteName.trim() || getConfigFallback(c, "SITE_NAME");
101
- return sse(c, async (stream)=>{
102
- if (languageChanged) {
103
- await stream.redirect("/dash/settings?saved");
104
- } else {
105
- const escaped = escapeHtml(displayName);
106
- await stream.patchElements(`<a id="site-name" href="/dash" class="font-semibold">${escaped}</a>`);
107
- await stream.patchElements(`Settings - ${escaped}`, {
108
- mode: "inner",
109
- selector: "title"
110
- });
111
- await stream.toast("Settings saved successfully.");
112
- await stream.patchSignals({
113
- _orig_siteName: body.siteName,
114
- _orig_siteDescription: body.siteDescription,
115
- _orig_siteLanguage: body.siteLanguage,
116
- _orig_homeDefaultView: body.homeDefaultView,
117
- _orig_timeZone: body.timeZone,
118
- _generalDirty: false
119
- });
120
- }
121
- });
122
- });
123
- settingsRoutes.post("/footer", async (c)=>{
124
- const body = await c.req.json();
125
- const { settings } = c.var.services;
126
- if (body.siteFooter?.trim()) {
127
- await settings.set("SITE_FOOTER", body.siteFooter.trim());
128
- } else {
129
- await settings.remove("SITE_FOOTER");
130
- }
131
- return sse(c, async (stream)=>{
132
- await stream.toast("Footer saved successfully.");
133
- await stream.patchSignals({
134
- _orig_siteFooter: body.siteFooter,
135
- _footerDirty: false
136
- });
137
- });
138
- });
139
- settingsRoutes.post("/seo", async (c)=>{
140
- const body = await c.req.json();
141
- const { settings } = c.var.services;
142
- // Checkbox "noindex" is the allow-indexing signal:
143
- // checked (value "true") = indexing allowed -> remove NOINDEX
144
- // unchecked (value "") = indexing blocked -> set NOINDEX=true
145
- if (body.noindex === "true") {
146
- await settings.remove("NOINDEX");
147
- } else {
148
- await settings.set("NOINDEX", "true");
149
- }
150
- return sse(c, async (stream)=>{
151
- await stream.toast("SEO settings saved successfully.");
152
- await stream.patchSignals({
153
- _orig_noindex: body.noindex,
154
- _seoDirty: false
155
- });
156
- });
157
- });
158
- // ===========================================================================
159
- // Avatar upload & removal
160
- // ===========================================================================
161
- settingsRoutes.post("/avatar", async (c)=>{
162
- const storage = c.var.storage;
163
- if (!storage) {
164
- return dsToast("Storage not configured.", "error");
165
- }
166
- const formData = await c.req.formData();
167
- const file = formData.get("file");
168
- if (!file) {
169
- return dsToast("No file provided.", "error");
170
- }
171
- const allowedTypes = [
172
- "image/jpeg",
173
- "image/png",
174
- "image/gif",
175
- "image/webp",
176
- "image/svg+xml"
177
- ];
178
- if (!allowedTypes.includes(file.type)) {
179
- return dsToast("File type not allowed.", "error");
180
- }
181
- const maxSize = 10 * 1024 * 1024;
182
- if (file.size > maxSize) {
183
- return dsToast("File too large (max 10MB).", "error");
184
- }
185
- const { uuidv7 } = await import("uuidv7");
186
- const ext = file.name.split(".").pop() || "bin";
187
- const id = uuidv7();
188
- const date = new Date();
189
- const year = date.getUTCFullYear();
190
- const month = String(date.getUTCMonth() + 1).padStart(2, "0");
191
- const filename = `${id}.${ext}`;
192
- const storageKey = `media/${year}/${month}/${filename}`;
193
- try {
194
- await storage.put(storageKey, file.stream(), {
195
- contentType: file.type
196
- });
197
- await c.var.services.media.create({
198
- id,
199
- filename,
200
- originalName: file.name,
201
- mimeType: file.type,
202
- size: file.size,
203
- storageKey,
204
- provider: c.env.STORAGE_DRIVER || "r2"
205
- });
206
- await c.var.services.settings.set("SITE_AVATAR", storageKey);
207
- // Store favicon variants as base64 in settings (small files, accessed every page load)
208
- const faviconFile = formData.get("favicon");
209
- const appleTouchFile = formData.get("appleTouch");
210
- if (faviconFile) {
211
- const b64 = arrayBufferToBase64(await faviconFile.arrayBuffer());
212
- await c.var.services.settings.set("SITE_FAVICON_ICO", b64);
213
- }
214
- if (appleTouchFile) {
215
- const b64 = arrayBufferToBase64(await appleTouchFile.arrayBuffer());
216
- await c.var.services.settings.set("SITE_FAVICON_APPLE_TOUCH", b64);
217
- }
218
- return dsRedirect("/dash/settings?saved");
219
- } catch {
220
- return dsToast("Upload failed. Please try again.", "error");
221
- }
222
- });
223
- settingsRoutes.post("/avatar/remove", async (c)=>{
224
- await c.var.services.settings.remove("SITE_AVATAR");
225
- await c.var.services.settings.remove("SITE_FAVICON_ICO");
226
- await c.var.services.settings.remove("SITE_FAVICON_APPLE_TOUCH");
227
- return dsRedirect("/dash/settings?saved");
228
- });
229
- settingsRoutes.post("/avatar/display", async (c)=>{
230
- const body = await c.req.json();
231
- const { settings } = c.var.services;
232
- if (body.showHeaderAvatar === "true") {
233
- await settings.set("SHOW_HEADER_AVATAR", "true");
234
- } else {
235
- await settings.remove("SHOW_HEADER_AVATAR");
236
- }
237
- return sse(c, async (stream)=>{
238
- await stream.toast("Avatar display setting saved successfully.");
239
- await stream.patchSignals({
240
- _orig_showHeaderAvatar: body.showHeaderAvatar,
241
- _avatarDisplayDirty: false
242
- });
243
- });
244
- });
245
- // ===========================================================================
246
- // Appearance
247
- // ===========================================================================
248
- settingsRoutes.get("/appearance", async (c)=>{
249
- const { settings } = c.var.services;
250
- const siteName = await getSiteName(c);
251
- const currentThemeId = await settings.get(SETTINGS_KEYS.THEME) ?? "default";
252
- const currentFontThemeId = await settings.get("FONT_THEME") ?? "default";
253
- const customCSS = await settings.get(SETTINGS_KEYS.CUSTOM_CSS) ?? "";
254
- const themes = getAvailableThemes(c.var.config);
255
- const saved = c.req.query("saved") !== undefined;
256
- return c.html(/*#__PURE__*/ _jsx(DashLayout, {
257
- c: c,
258
- title: "Settings",
259
- siteName: siteName,
260
- currentPath: "/dash/settings",
261
- toast: saved ? {
262
- message: "Theme saved successfully."
263
- } : undefined,
264
- children: /*#__PURE__*/ _jsx(AppearanceContent, {
265
- themes: themes,
266
- currentThemeId: currentThemeId,
267
- fontThemes: BUILTIN_FONT_THEMES,
268
- currentFontThemeId: currentFontThemeId,
269
- customCSS: customCSS
270
- })
271
- }));
272
- });
273
- settingsRoutes.post("/appearance", async (c)=>{
274
- const body = await c.req.json();
275
- const { settings } = c.var.services;
276
- const themes = getAvailableThemes(c.var.config);
277
- const validTheme = themes.find((t)=>t.id === body.theme);
278
- if (!validTheme) {
279
- return dsToast("Invalid theme selected.", "error");
280
- }
281
- if (validTheme.id === "default") {
282
- await settings.remove(SETTINGS_KEYS.THEME);
283
- } else {
284
- await settings.set(SETTINGS_KEYS.THEME, validTheme.id);
285
- }
286
- return dsRedirect("/dash/settings/appearance?saved");
287
- });
288
- settingsRoutes.post("/font-theme", async (c)=>{
289
- const body = await c.req.json();
290
- const { settings } = c.var.services;
291
- const validFont = BUILTIN_FONT_THEMES.find((f)=>f.id === body.fontTheme);
292
- if (!validFont) {
293
- return dsToast("Invalid font theme selected.", "error");
294
- }
295
- if (validFont.id === "default") {
296
- await settings.remove("FONT_THEME");
297
- } else {
298
- await settings.set("FONT_THEME", validFont.id);
299
- }
300
- return dsRedirect("/dash/settings/appearance?saved");
301
- });
302
- settingsRoutes.post("/custom-css", async (c)=>{
303
- const body = await c.req.json();
304
- const { settings } = c.var.services;
305
- const css = body.customCSS?.trim() ?? "";
306
- if (css) {
307
- await settings.set(SETTINGS_KEYS.CUSTOM_CSS, css);
308
- } else {
309
- await settings.remove(SETTINGS_KEYS.CUSTOM_CSS);
310
- }
311
- return dsToast("Custom CSS saved successfully.");
312
- });
313
- // ===========================================================================
314
- // Account
315
- // ===========================================================================
316
- settingsRoutes.get("/account", async (c)=>{
317
- const siteName = await getSiteName(c);
318
- const session = await c.var.auth.api.getSession({
319
- headers: c.req.raw.headers
320
- });
321
- const userName = session?.user?.name ?? "";
322
- const saved = c.req.query("saved") !== undefined;
323
- return c.html(/*#__PURE__*/ _jsx(DashLayout, {
324
- c: c,
325
- title: "Settings",
326
- siteName: siteName,
327
- currentPath: "/dash/settings",
328
- toast: saved ? {
329
- message: "Profile saved successfully."
330
- } : undefined,
331
- children: /*#__PURE__*/ _jsx(AccountContent, {
332
- userName: userName
333
- })
334
- }));
335
- });
336
- settingsRoutes.post("/account", async (c)=>{
337
- const body = await c.req.json();
338
- const name = body.userName?.trim();
339
- if (!name) {
340
- return dsToast("Name is required.", "error");
341
- }
342
- try {
343
- await c.var.auth.api.updateUser({
344
- body: {
345
- name
346
- },
347
- headers: c.req.raw.headers
348
- });
349
- } catch {
350
- return dsToast("Failed to update profile.", "error");
351
- }
352
- return dsToast("Profile saved successfully.");
353
- });
354
- settingsRoutes.post("/password", async (c)=>{
355
- const body = await c.req.json();
356
- if (body.newPassword !== body.confirmPassword) {
357
- return dsToast("Passwords do not match.", "error");
358
- }
359
- try {
360
- await c.var.auth.api.changePassword({
361
- body: {
362
- currentPassword: body.currentPassword,
363
- newPassword: body.newPassword,
364
- revokeOtherSessions: false
365
- },
366
- headers: c.req.raw.headers
367
- });
368
- } catch {
369
- return dsToast("Current password is incorrect.", "error");
370
- }
371
- return sse(c, async (stream)=>{
372
- await stream.toast("Password changed successfully.");
373
- await stream.patchSignals({
374
- currentPassword: "",
375
- newPassword: "",
376
- confirmPassword: ""
377
- });
378
- });
379
- });
@@ -1,62 +0,0 @@
1
- /**
2
- * RSS Feed Routes
3
- */ import { Hono } from "hono";
4
- import { defaultRssRenderer, defaultAtomRenderer } from "../../lib/feed.js";
5
- import { getSiteLanguage } from "../../lib/config.js";
6
- import { buildMediaMap } from "../../lib/media-helpers.js";
7
- import { createMediaContext, toPostViews } from "../../lib/view.js";
8
- export const rssRoutes = new Hono();
9
- /**
10
- * Build FeedData from the Hono context.
11
- */ async function buildFeedData(c) {
12
- const all = await c.var.services.settings.getAll();
13
- const siteName = all["SITE_NAME"] ?? "Jant";
14
- const siteDescription = all["SITE_DESCRIPTION"] ?? "";
15
- const siteUrl = c.env.SITE_URL;
16
- const siteLanguage = await getSiteLanguage(c);
17
- const feedLimit = parseInt(c.env.RSS_FEED_LIMIT ?? "50", 10) || 50;
18
- const posts = await c.var.services.posts.list({
19
- status: "published",
20
- excludeReplies: true,
21
- limit: feedLimit
22
- });
23
- // Batch load media for enclosures
24
- const postIds = posts.map((p)=>p.id);
25
- const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
26
- const mediaCtx = createMediaContext(c);
27
- const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
28
- // Transform to PostView[] with media
29
- const postViews = toPostViews(posts.map((p)=>({
30
- ...p,
31
- mediaAttachments: mediaMap.get(p.id) ?? []
32
- })), mediaCtx);
33
- return {
34
- siteName,
35
- siteDescription,
36
- siteUrl,
37
- siteLanguage,
38
- posts: postViews
39
- };
40
- }
41
- // RSS 2.0 Feed - main feed at /feed
42
- rssRoutes.get("/", async (c)=>{
43
- const feedData = await buildFeedData(c);
44
- const renderer = c.var.config.feed?.rss ?? defaultRssRenderer;
45
- const xml = renderer(feedData);
46
- return new Response(xml, {
47
- headers: {
48
- "Content-Type": "application/rss+xml; charset=utf-8"
49
- }
50
- });
51
- });
52
- // Atom Feed
53
- rssRoutes.get("/atom.xml", async (c)=>{
54
- const feedData = await buildFeedData(c);
55
- const renderer = c.var.config.feed?.atom ?? defaultAtomRenderer;
56
- const xml = renderer(feedData);
57
- return new Response(xml, {
58
- headers: {
59
- "Content-Type": "application/atom+xml; charset=utf-8"
60
- }
61
- });
62
- });
@@ -1,49 +0,0 @@
1
- /**
2
- * Sitemap Routes
3
- */ import { Hono } from "hono";
4
- import { defaultSitemapRenderer } from "../../lib/feed.js";
5
- import { createMediaContext, toPostViewsFromPosts, toPageView } from "../../lib/view.js";
6
- export const sitemapRoutes = new Hono();
7
- // XML Sitemap
8
- sitemapRoutes.get("/sitemap.xml", async (c)=>{
9
- const siteUrl = c.env.SITE_URL;
10
- const posts = await c.var.services.posts.list({
11
- status: "published",
12
- excludeReplies: true,
13
- limit: 1000
14
- });
15
- // Fetch published pages
16
- const allPages = await c.var.services.pages.list();
17
- const publishedPages = allPages.filter((p)=>p.status === "published");
18
- // Transform to View Models
19
- const mediaCtx = createMediaContext(c);
20
- const postViews = toPostViewsFromPosts(posts, mediaCtx);
21
- const pageViews = publishedPages.map(toPageView);
22
- const renderer = c.var.config.feed?.sitemap ?? defaultSitemapRenderer;
23
- const xml = renderer({
24
- siteUrl,
25
- posts: postViews,
26
- pages: pageViews
27
- });
28
- return new Response(xml, {
29
- headers: {
30
- "Content-Type": "application/xml; charset=utf-8"
31
- }
32
- });
33
- });
34
- // robots.txt
35
- sitemapRoutes.get("/robots.txt", async (c)=>{
36
- const siteUrl = c.env.SITE_URL;
37
- const noindex = await c.var.services.settings.get("NOINDEX") === "true";
38
- const directive = noindex ? "Disallow: /" : "Allow: /";
39
- const robots = `User-agent: *
40
- ${directive}
41
-
42
- Sitemap: ${siteUrl}/sitemap.xml
43
- `;
44
- return new Response(robots, {
45
- headers: {
46
- "Content-Type": "text/plain; charset=utf-8"
47
- }
48
- });
49
- });
@@ -1,62 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Archive Page Route
4
- *
5
- * Shows all posts, optionally filtered by format or featured status
6
- */ import { Hono } from "hono";
7
- import { FORMATS } from "../../types.js";
8
- import { ArchivePage } from "../../ui/pages/ArchivePage.js";
9
- import { getNavigationData } from "../../lib/navigation.js";
10
- import { renderPublicPage } from "../../lib/render.js";
11
- import { createMediaContext, toArchiveGroups } from "../../lib/view.js";
12
- const PAGE_SIZE = 50;
13
- export const archiveRoutes = new Hono();
14
- // Archive page - all posts
15
- archiveRoutes.get("/", async (c)=>{
16
- const formatParam = c.req.query("format");
17
- const format = formatParam && FORMATS.includes(formatParam) ? formatParam : undefined;
18
- const featuredParam = c.req.query("featured");
19
- const featured = featuredParam === "true" ? true : undefined;
20
- // Parse cursor
21
- const cursorParam = c.req.query("cursor");
22
- const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
23
- const navData = await getNavigationData(c);
24
- // Fetch one extra to check for more
25
- const posts = await c.var.services.posts.list({
26
- format,
27
- status: "published",
28
- featured,
29
- excludeReplies: true,
30
- cursor,
31
- limit: PAGE_SIZE + 1
32
- });
33
- const hasMore = posts.length > PAGE_SIZE;
34
- const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
35
- // Get next cursor
36
- const nextCursor = hasMore && displayPosts.length > 0 ? displayPosts[displayPosts.length - 1].id : undefined;
37
- // Group posts by year-month
38
- const grouped = new Map();
39
- for (const post of displayPosts){
40
- const date = new Date(post.publishedAt * 1000);
41
- const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
42
- if (!grouped.has(key)) {
43
- grouped.set(key, []);
44
- }
45
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Map.set() above guarantees key exists
46
- grouped.get(key).push(post);
47
- }
48
- // Transform to View Models
49
- const mediaCtx = createMediaContext(c);
50
- const groups = toArchiveGroups(grouped, mediaCtx);
51
- return renderPublicPage(c, {
52
- title: `Archive - ${navData.siteName}`,
53
- navData,
54
- content: /*#__PURE__*/ _jsx(ArchivePage, {
55
- groups: groups,
56
- hasMore: hasMore,
57
- nextCursor: nextCursor,
58
- format: format,
59
- featured: featured
60
- })
61
- });
62
- });
@@ -1,34 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Collection Page Route
4
- */ import { Hono } from "hono";
5
- import { CollectionPage } from "../../ui/pages/CollectionPage.js";
6
- import { getNavigationData } from "../../lib/navigation.js";
7
- import { renderPublicPage } from "../../lib/render.js";
8
- import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
9
- export const collectionRoutes = new Hono();
10
- collectionRoutes.get("/:slug", async (c)=>{
11
- const slug = c.req.param("slug");
12
- const collection = await c.var.services.collections.getBySlug(slug);
13
- if (!collection) return c.notFound();
14
- // Fetch posts in this collection
15
- const posts = await c.var.services.posts.list({
16
- collectionId: collection.id,
17
- status: "published",
18
- excludeReplies: true
19
- });
20
- const navData = await getNavigationData(c);
21
- // Transform to View Models
22
- const mediaCtx = createMediaContext(c);
23
- const postViews = toPostViewsFromPosts(posts, mediaCtx);
24
- return renderPublicPage(c, {
25
- title: `${collection.title} - ${navData.siteName}`,
26
- description: collection.description ?? undefined,
27
- navData,
28
- content: /*#__PURE__*/ _jsx(CollectionPage, {
29
- collection: collection,
30
- posts: postViews,
31
- hasMore: false
32
- })
33
- });
34
- });
@@ -1,28 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Collections Listing Page Route
4
- *
5
- * Lists all collections with their post counts.
6
- */ import { Hono } from "hono";
7
- import { getNavigationData } from "../../lib/navigation.js";
8
- import { renderPublicPage } from "../../lib/render.js";
9
- import { CollectionsPage } from "../../ui/pages/CollectionsPage.js";
10
- export const collectionsPageRoutes = new Hono();
11
- collectionsPageRoutes.get("/", async (c)=>{
12
- const [allCollections, postCounts] = await Promise.all([
13
- c.var.services.collections.list(),
14
- c.var.services.collections.getPostCounts()
15
- ]);
16
- const collections = allCollections.map((col)=>({
17
- ...col,
18
- postCount: postCounts.get(col.id) ?? 0
19
- }));
20
- const navData = await getNavigationData(c);
21
- return renderPublicPage(c, {
22
- title: `Collections - ${navData.siteName}`,
23
- navData,
24
- content: /*#__PURE__*/ _jsx(CollectionsPage, {
25
- collections: collections
26
- })
27
- });
28
- });
@@ -1,36 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Featured Page Route
4
- *
5
- * Shows featured posts as a timeline feed.
6
- */ import { Hono } from "hono";
7
- import { getNavigationData } from "../../lib/navigation.js";
8
- import { renderPublicPage } from "../../lib/render.js";
9
- import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
10
- import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
11
- export const featuredRoutes = new Hono();
12
- featuredRoutes.get("/", async (c)=>{
13
- const navData = await getNavigationData(c);
14
- // When homepage already shows featured, redirect to avoid duplicate content
15
- if (navData.homeDefaultView === "featured") {
16
- return c.redirect("/", 302);
17
- }
18
- const posts = await c.var.services.posts.list({
19
- featured: true,
20
- status: "published",
21
- excludeReplies: true
22
- });
23
- const mediaCtx = createMediaContext(c);
24
- const postViews = toPostViewsFromPosts(posts, mediaCtx);
25
- // Convert to timeline items (simple — no thread previews)
26
- const items = postViews.map((post)=>({
27
- post
28
- }));
29
- return renderPublicPage(c, {
30
- title: `Featured - ${navData.siteName}`,
31
- navData,
32
- content: /*#__PURE__*/ _jsx(FeaturedPage, {
33
- items: items
34
- })
35
- });
36
- });