@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
@@ -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
- });