@jant/core 0.3.27 → 0.3.29

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/bin/reset-password.js +22 -0
  2. package/dist/client/client.css +1 -0
  3. package/dist/client/client.js +31561 -0
  4. package/dist/index.js +15209 -15
  5. package/package.json +25 -15
  6. package/src/__tests__/helpers/app.ts +19 -3
  7. package/src/__tests__/helpers/db.ts +44 -0
  8. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  9. package/src/app.tsx +111 -174
  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 -267
  172. package/dist/auth.js +0 -39
  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
@@ -4,10 +4,9 @@
4
4
 
5
5
  import { Hono } from "hono";
6
6
  import type { Bindings } from "../../types.js";
7
- import type { AppVariables } from "../../app.js";
7
+ import type { AppVariables } from "../../types/app-context.js";
8
8
  import { DashLayout } from "../../ui/layouts/DashLayout.js";
9
9
  import { dsRedirect } from "../../lib/sse.js";
10
- import { getSiteName } from "../../lib/config.js";
11
10
  import {
12
11
  getMediaUrl,
13
12
  getImageUrl,
@@ -22,8 +21,8 @@ export const mediaRoutes = new Hono<Env>();
22
21
 
23
22
  // List media
24
23
  mediaRoutes.get("/", async (c) => {
25
- const mediaList = await c.var.services.media.list(100);
26
- const siteName = await getSiteName(c);
24
+ const mediaList = await c.var.services.media.list({ limit: 100 });
25
+ const siteName = c.var.appConfig.siteName;
27
26
 
28
27
  return c.html(
29
28
  <DashLayout
@@ -34,9 +33,9 @@ mediaRoutes.get("/", async (c) => {
34
33
  >
35
34
  <MediaListContent
36
35
  mediaList={mediaList}
37
- r2PublicUrl={c.env.R2_PUBLIC_URL}
38
- imageTransformUrl={c.env.IMAGE_TRANSFORM_URL}
39
- s3PublicUrl={c.env.S3_PUBLIC_URL}
36
+ r2PublicUrl={c.var.appConfig.r2PublicUrl}
37
+ imageTransformUrl={c.var.appConfig.imageTransformUrl}
38
+ s3PublicUrl={c.var.appConfig.s3PublicUrl}
40
39
  />
41
40
  </DashLayout>,
42
41
  );
@@ -45,10 +44,13 @@ mediaRoutes.get("/", async (c) => {
45
44
  // Media picker (returns HTML fragment for PostForm dialog)
46
45
  // Must be defined before /:id to avoid "picker" matching as an ID
47
46
  mediaRoutes.get("/picker", async (c) => {
48
- const mediaList = await c.var.services.media.list(100);
49
- const r2PublicUrl = c.env.R2_PUBLIC_URL;
50
- const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
51
- const s3PublicUrl = c.env.S3_PUBLIC_URL;
47
+ const mediaList = await c.var.services.media.list({
48
+ limit: 100,
49
+ mimePrefix: "image/",
50
+ });
51
+ const r2PublicUrl = c.var.appConfig.r2PublicUrl;
52
+ const imageTransformUrl = c.var.appConfig.imageTransformUrl;
53
+ const s3PublicUrl = c.var.appConfig.s3PublicUrl;
52
54
 
53
55
  if (mediaList.length === 0) {
54
56
  return c.html(
@@ -60,40 +62,35 @@ mediaRoutes.get("/picker", async (c) => {
60
62
 
61
63
  return c.html(
62
64
  <>
63
- {mediaList
64
- .filter((m) => m.mimeType.startsWith("image/"))
65
- .map((m) => {
66
- const pUrl = getPublicUrlForProvider(
67
- m.provider,
68
- r2PublicUrl,
69
- s3PublicUrl,
70
- );
71
- const url = getMediaUrl(m.storageKey, pUrl);
72
- const thumbUrl = getImageUrl(url, imageTransformUrl, {
73
- width: 150,
74
- quality: 80,
75
- format: "auto",
76
- fit: "cover",
77
- });
78
- return (
79
- <button
80
- key={m.id}
81
- type="button"
82
- class="aspect-square rounded-lg overflow-hidden border-2 hover:border-primary cursor-pointer transition-colors"
83
- data-on:click={`$mediaIds.includes('${m.id}') ? ($mediaIds = $mediaIds.filter(id => id !== '${m.id}')) : ($mediaIds = [...$mediaIds, '${m.id}'])`}
84
- data-class:border-primary={`$mediaIds.includes('${m.id}')`}
85
- data-class:ring-2={`$mediaIds.includes('${m.id}')`}
86
- data-class:ring-primary={`$mediaIds.includes('${m.id}')`}
87
- >
88
- <img
89
- src={thumbUrl}
90
- alt={m.alt || m.originalName}
91
- class="w-full h-full object-cover"
92
- loading="lazy"
93
- />
94
- </button>
95
- );
96
- })}
65
+ {mediaList.map((m) => {
66
+ const pUrl = getPublicUrlForProvider(
67
+ m.provider,
68
+ r2PublicUrl,
69
+ s3PublicUrl,
70
+ );
71
+ const url = getMediaUrl(m.storageKey, pUrl);
72
+ const thumbUrl = getImageUrl(url, imageTransformUrl, {
73
+ width: 150,
74
+ quality: 80,
75
+ format: "auto",
76
+ fit: "cover",
77
+ });
78
+ return (
79
+ <button
80
+ key={m.id}
81
+ type="button"
82
+ class="aspect-square rounded-lg overflow-hidden border-2 hover:border-primary cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
83
+ data-media-id={m.id}
84
+ >
85
+ <img
86
+ src={thumbUrl}
87
+ alt={m.alt || m.originalName}
88
+ class="w-full h-full object-cover"
89
+ loading="lazy"
90
+ />
91
+ </button>
92
+ );
93
+ })}
97
94
  </>,
98
95
  );
99
96
  });
@@ -104,7 +101,7 @@ mediaRoutes.get("/:id", async (c) => {
104
101
  const media = await c.var.services.media.getById(id);
105
102
  if (!media) return c.notFound();
106
103
 
107
- const siteName = await getSiteName(c);
104
+ const siteName = c.var.appConfig.siteName;
108
105
 
109
106
  return c.html(
110
107
  <DashLayout
@@ -115,9 +112,9 @@ mediaRoutes.get("/:id", async (c) => {
115
112
  >
116
113
  <ViewMediaContent
117
114
  media={media}
118
- r2PublicUrl={c.env.R2_PUBLIC_URL}
119
- imageTransformUrl={c.env.IMAGE_TRANSFORM_URL}
120
- s3PublicUrl={c.env.S3_PUBLIC_URL}
115
+ r2PublicUrl={c.var.appConfig.r2PublicUrl}
116
+ imageTransformUrl={c.var.appConfig.imageTransformUrl}
117
+ s3PublicUrl={c.var.appConfig.s3PublicUrl}
121
118
  />
122
119
  </DashLayout>,
123
120
  );
@@ -5,15 +5,17 @@
5
5
  */
6
6
 
7
7
  import { Hono } from "hono";
8
+ import { msg } from "@lingui/core/macro";
8
9
  import { useLingui } from "@lingui/react/macro";
9
10
  import type { Bindings, Page } from "../../types.js";
10
- import type { AppVariables } from "../../app.js";
11
+ import type { AppVariables } from "../../types/app-context.js";
11
12
  import { DashLayout } from "../../ui/layouts/DashLayout.js";
12
13
  import { PageForm, ActionButtons, DangerZone } from "../../ui/dash/index.js";
13
14
  import { dsRedirect, dsToast } from "../../lib/sse.js";
14
- import { getSiteName } from "../../lib/config.js";
15
+ import { CreatePageSchema } from "../../lib/schemas.js";
15
16
  import { UnifiedPagesContent } from "../../ui/dash/pages/UnifiedPagesContent.js";
16
17
  import { LinkFormContent } from "../../ui/dash/pages/LinkFormContent.js";
18
+ import { getI18n } from "../../i18n/index.js";
17
19
 
18
20
  type Env = { Bindings: Bindings; Variables: AppVariables };
19
21
 
@@ -109,7 +111,7 @@ pagesRoutes.get("/", async (c) => {
109
111
  c.var.services.navItems.list(),
110
112
  c.var.services.pages.listNotInNav(),
111
113
  ]);
112
- const siteName = await getSiteName(c);
114
+ const siteName = c.var.appConfig.siteName;
113
115
 
114
116
  return c.html(
115
117
  <DashLayout
@@ -124,7 +126,7 @@ pagesRoutes.get("/", async (c) => {
124
126
  });
125
127
 
126
128
  pagesRoutes.get("/new", async (c) => {
127
- const siteName = await getSiteName(c);
129
+ const siteName = c.var.appConfig.siteName;
128
130
  return c.html(
129
131
  <DashLayout
130
132
  c={c}
@@ -138,7 +140,7 @@ pagesRoutes.get("/new", async (c) => {
138
140
  });
139
141
 
140
142
  pagesRoutes.get("/links/new", async (c) => {
141
- const siteName = await getSiteName(c);
143
+ const siteName = c.var.appConfig.siteName;
142
144
  return c.html(
143
145
  <DashLayout
144
146
  c={c}
@@ -152,9 +154,18 @@ pagesRoutes.get("/links/new", async (c) => {
152
154
  });
153
155
 
154
156
  pagesRoutes.post("/links", async (c) => {
157
+ const i18n = getI18n(c);
155
158
  const body = await c.req.json<{ label: string; url: string }>();
156
159
  if (!body.label || !body.url) {
157
- return dsToast("Label and URL are required", "error");
160
+ return dsToast(
161
+ i18n._(
162
+ msg({
163
+ message: "Label and URL are required",
164
+ comment: "@context: Error toast when nav link fields are empty",
165
+ }),
166
+ ),
167
+ "error",
168
+ );
158
169
  }
159
170
 
160
171
  await c.var.services.navItems.create({
@@ -166,12 +177,28 @@ pagesRoutes.post("/links", async (c) => {
166
177
  });
167
178
 
168
179
  pagesRoutes.post("/reorder", async (c) => {
180
+ const i18n = getI18n(c);
169
181
  const body = await c.req.json<{ ids: number[] }>();
170
182
  if (!Array.isArray(body.ids)) {
171
- return dsToast("Invalid request", "error");
183
+ return dsToast(
184
+ i18n._(
185
+ msg({
186
+ message: "Invalid request",
187
+ comment: "@context: Error toast when reorder request is malformed",
188
+ }),
189
+ ),
190
+ "error",
191
+ );
172
192
  }
173
193
  await c.var.services.navItems.reorder(body.ids);
174
- return dsToast("Order saved");
194
+ return dsToast(
195
+ i18n._(
196
+ msg({
197
+ message: "Order saved",
198
+ comment: "@context: Toast after saving navigation item order",
199
+ }),
200
+ ),
201
+ );
175
202
  });
176
203
 
177
204
  pagesRoutes.get("/links/:id/edit", async (c) => {
@@ -181,7 +208,7 @@ pagesRoutes.get("/links/:id/edit", async (c) => {
181
208
  const item = await c.var.services.navItems.getById(id);
182
209
  if (!item) return c.notFound();
183
210
 
184
- const siteName = await getSiteName(c);
211
+ const siteName = c.var.appConfig.siteName;
185
212
  return c.html(
186
213
  <DashLayout
187
214
  c={c}
@@ -195,12 +222,21 @@ pagesRoutes.get("/links/:id/edit", async (c) => {
195
222
  });
196
223
 
197
224
  pagesRoutes.post("/links/:id", async (c) => {
225
+ const i18n = getI18n(c);
198
226
  const id = parseInt(c.req.param("id"), 10);
199
227
  if (isNaN(id)) return c.notFound();
200
228
 
201
229
  const body = await c.req.json<{ label: string; url: string }>();
202
230
  if (!body.label || !body.url) {
203
- return dsToast("Label and URL are required", "error");
231
+ return dsToast(
232
+ i18n._(
233
+ msg({
234
+ message: "Label and URL are required",
235
+ comment: "@context: Error toast when nav link fields are empty",
236
+ }),
237
+ ),
238
+ "error",
239
+ );
204
240
  }
205
241
 
206
242
  const updated = await c.var.services.navItems.update(id, {
@@ -221,18 +257,26 @@ pagesRoutes.post("/links/:id/delete", async (c) => {
221
257
  });
222
258
 
223
259
  pagesRoutes.post("/", async (c) => {
224
- const body = await c.req.json<{
225
- title: string;
226
- body: string;
227
- status: string;
228
- slug: string;
229
- }>();
260
+ const i18n = getI18n(c);
261
+ const raw = await c.req.json();
262
+ const parsed = CreatePageSchema.safeParse(raw);
263
+ if (!parsed.success) {
264
+ const errorMsg =
265
+ parsed.error.issues[0]?.message ??
266
+ i18n._(
267
+ msg({
268
+ message: "Invalid input",
269
+ comment: "@context: Fallback validation error for page form",
270
+ }),
271
+ );
272
+ return dsToast(errorMsg, "error");
273
+ }
230
274
 
231
275
  const page = await c.var.services.pages.create({
232
- title: body.title,
233
- body: body.body,
234
- status: body.status as Page["status"],
235
- slug: body.slug.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
276
+ title: parsed.data.title,
277
+ body: parsed.data.body,
278
+ status: parsed.data.status,
279
+ slug: parsed.data.slug,
236
280
  });
237
281
 
238
282
  return dsRedirect(`/dash/pages/${page.id}`);
@@ -258,11 +302,7 @@ pagesRoutes.post("/:id/remove-from-nav", async (c) => {
258
302
  const pageId = parseInt(c.req.param("id"), 10);
259
303
  if (isNaN(pageId)) return c.notFound();
260
304
 
261
- const navItems = await c.var.services.navItems.list();
262
- const navItem = navItems.find((item) => item.pageId === pageId);
263
- if (navItem) {
264
- await c.var.services.navItems.delete(navItem.id);
265
- }
305
+ await c.var.services.navItems.deleteByPageId(pageId);
266
306
  return dsRedirect("/dash/pages");
267
307
  });
268
308
 
@@ -273,7 +313,7 @@ pagesRoutes.get("/:id", async (c) => {
273
313
  const page = await c.var.services.pages.getById(id);
274
314
  if (!page) return c.notFound();
275
315
 
276
- const siteName = await getSiteName(c);
316
+ const siteName = c.var.appConfig.siteName;
277
317
  return c.html(
278
318
  <DashLayout
279
319
  c={c}
@@ -293,7 +333,7 @@ pagesRoutes.get("/:id/edit", async (c) => {
293
333
  const page = await c.var.services.pages.getById(id);
294
334
  if (!page) return c.notFound();
295
335
 
296
- const siteName = await getSiteName(c);
336
+ const siteName = c.var.appConfig.siteName;
297
337
  return c.html(
298
338
  <DashLayout
299
339
  c={c}
@@ -307,21 +347,29 @@ pagesRoutes.get("/:id/edit", async (c) => {
307
347
  });
308
348
 
309
349
  pagesRoutes.post("/:id", async (c) => {
350
+ const i18n = getI18n(c);
310
351
  const id = parseInt(c.req.param("id"), 10);
311
352
  if (isNaN(id)) return c.notFound();
312
353
 
313
- const body = await c.req.json<{
314
- title: string;
315
- body: string;
316
- status: string;
317
- slug: string;
318
- }>();
354
+ const raw = await c.req.json();
355
+ const parsed = CreatePageSchema.safeParse(raw);
356
+ if (!parsed.success) {
357
+ const errorMsg =
358
+ parsed.error.issues[0]?.message ??
359
+ i18n._(
360
+ msg({
361
+ message: "Invalid input",
362
+ comment: "@context: Fallback validation error for page form",
363
+ }),
364
+ );
365
+ return dsToast(errorMsg, "error");
366
+ }
319
367
 
320
368
  await c.var.services.pages.update(id, {
321
- title: body.title,
322
- body: body.body,
323
- status: body.status as Page["status"],
324
- slug: body.slug.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
369
+ title: parsed.data.title,
370
+ body: parsed.data.body,
371
+ status: parsed.data.status,
372
+ slug: parsed.data.slug,
325
373
  });
326
374
 
327
375
  return dsRedirect(`/dash/pages/${id}`);
@@ -1,12 +1,17 @@
1
- import { getSiteName } from "../../lib/config.js";
2
1
  /**
3
2
  * Dashboard Posts Routes
4
3
  */
5
4
 
6
5
  import { Hono } from "hono";
7
6
  import { useLingui } from "@lingui/react/macro";
8
- import type { Bindings, Post, Media, Collection } from "../../types.js";
9
- import type { AppVariables } from "../../app.js";
7
+ import type {
8
+ Bindings,
9
+ Post,
10
+ PostView,
11
+ Media,
12
+ Collection,
13
+ } from "../../types.js";
14
+ import type { AppVariables } from "../../types/app-context.js";
10
15
  import { DashLayout } from "../../ui/layouts/DashLayout.js";
11
16
  import {
12
17
  PostForm,
@@ -16,12 +21,17 @@ import {
16
21
  } from "../../ui/dash/index.js";
17
22
  import * as sqid from "../../lib/sqid.js";
18
23
  import { dsRedirect } from "../../lib/sse.js";
24
+ import {
25
+ toPostViewsFromPosts,
26
+ toPostViewFromPost,
27
+ createMediaContext,
28
+ } from "../../lib/view.js";
19
29
 
20
30
  type Env = { Bindings: Bindings; Variables: AppVariables };
21
31
 
22
32
  export const postsRoutes = new Hono<Env>();
23
33
 
24
- function PostsListContent({ posts }: { posts: Post[] }) {
34
+ function PostsListContent({ posts }: { posts: PostView[] }) {
25
35
  const { t } = useLingui();
26
36
  return (
27
37
  <>
@@ -55,7 +65,11 @@ postsRoutes.get("/", async (c) => {
55
65
  const posts = await c.var.services.posts.list({
56
66
  excludeReplies: true,
57
67
  });
58
- const siteName = await getSiteName(c);
68
+ const siteName = c.var.appConfig.siteName;
69
+ const postViews = toPostViewsFromPosts(
70
+ posts,
71
+ createMediaContext(c.var.appConfig),
72
+ );
59
73
 
60
74
  return c.html(
61
75
  <DashLayout
@@ -64,14 +78,14 @@ postsRoutes.get("/", async (c) => {
64
78
  siteName={siteName}
65
79
  currentPath="/dash/posts"
66
80
  >
67
- <PostsListContent posts={posts} />
81
+ <PostsListContent posts={postViews} />
68
82
  </DashLayout>,
69
83
  );
70
84
  });
71
85
 
72
86
  // New post form
73
87
  postsRoutes.get("/new", async (c) => {
74
- const siteName = await getSiteName(c);
88
+ const siteName = c.var.appConfig.siteName;
75
89
  const collections = await c.var.services.collections.list();
76
90
 
77
91
  return c.html(
@@ -88,6 +102,7 @@ postsRoutes.get("/new", async (c) => {
88
102
 
89
103
  // Create post
90
104
  postsRoutes.post("/", async (c) => {
105
+ const wantsJson = c.req.header("Accept")?.includes("application/json");
91
106
  const body = await c.req.json<{
92
107
  format: string;
93
108
  title?: string;
@@ -98,7 +113,7 @@ postsRoutes.post("/", async (c) => {
98
113
  url?: string;
99
114
  quoteText?: string;
100
115
  rating?: number;
101
- collectionId?: number;
116
+ collectionIds?: number[];
102
117
  mediaIds?: string[];
103
118
  }>();
104
119
 
@@ -112,7 +127,7 @@ postsRoutes.post("/", async (c) => {
112
127
  url: body.url || undefined,
113
128
  quoteText: body.quoteText || undefined,
114
129
  rating: body.rating || undefined,
115
- collectionId: body.collectionId || undefined,
130
+ collectionIds: body.collectionIds?.length ? body.collectionIds : undefined,
116
131
  });
117
132
 
118
133
  // Attach media if provided
@@ -120,16 +135,20 @@ postsRoutes.post("/", async (c) => {
120
135
  await c.var.services.media.attachToPost(post.id, body.mediaIds);
121
136
  }
122
137
 
123
- return dsRedirect(`/dash/posts/${sqid.encode(post.id)}`);
138
+ const redirectUrl = `/dash/posts/${sqid.encode(post.id)}`;
139
+ if (wantsJson) {
140
+ return c.json({ status: "redirect" as const, url: redirectUrl });
141
+ }
142
+
143
+ return dsRedirect(redirectUrl);
124
144
  });
125
145
 
126
- function ViewPostContent({ post }: { post: Post }) {
146
+ function ViewPostContent({ post }: { post: PostView }) {
127
147
  const { t } = useLingui();
128
148
  const defaultTitle = t({
129
149
  message: "Post",
130
150
  comment: "@context: Default post title",
131
151
  });
132
- const permalink = post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`;
133
152
 
134
153
  return (
135
154
  <>
@@ -141,7 +160,7 @@ function ViewPostContent({ post }: { post: Post }) {
141
160
  message: "Edit",
142
161
  comment: "@context: Button to edit post",
143
162
  })}
144
- viewHref={permalink}
163
+ viewHref={post.permalink}
145
164
  viewLabel={t({
146
165
  message: "View",
147
166
  comment: "@context: Button to view post",
@@ -168,6 +187,7 @@ function EditPostContent({
168
187
  imageTransformUrl,
169
188
  s3PublicUrl,
170
189
  collections,
190
+ postCollectionIds,
171
191
  }: {
172
192
  post: Post;
173
193
  mediaAttachments: Media[];
@@ -175,6 +195,7 @@ function EditPostContent({
175
195
  imageTransformUrl?: string;
176
196
  s3PublicUrl?: string;
177
197
  collections: Collection[];
198
+ postCollectionIds: number[];
178
199
  }) {
179
200
  const { t } = useLingui();
180
201
  return (
@@ -190,6 +211,8 @@ function EditPostContent({
190
211
  imageTransformUrl={imageTransformUrl}
191
212
  s3PublicUrl={s3PublicUrl}
192
213
  collections={collections}
214
+ postCollectionIds={postCollectionIds}
215
+ cancelHref={`/dash/posts/${sqid.encode(post.id)}`}
193
216
  />
194
217
  </>
195
218
  );
@@ -203,8 +226,12 @@ postsRoutes.get("/:id", async (c) => {
203
226
  const post = await c.var.services.posts.getById(id);
204
227
  if (!post) return c.notFound();
205
228
 
206
- const siteName = await getSiteName(c);
229
+ const siteName = c.var.appConfig.siteName;
207
230
  const pageTitle = post.title || "Post";
231
+ const postView = toPostViewFromPost(
232
+ post,
233
+ createMediaContext(c.var.appConfig),
234
+ );
208
235
 
209
236
  return c.html(
210
237
  <DashLayout
@@ -213,7 +240,7 @@ postsRoutes.get("/:id", async (c) => {
213
240
  siteName={siteName}
214
241
  currentPath="/dash/posts"
215
242
  >
216
- <ViewPostContent post={post} />
243
+ <ViewPostContent post={postView} />
217
244
  </DashLayout>,
218
245
  );
219
246
  });
@@ -226,12 +253,14 @@ postsRoutes.get("/:id/edit", async (c) => {
226
253
  const post = await c.var.services.posts.getById(id);
227
254
  if (!post) return c.notFound();
228
255
 
229
- const siteName = await getSiteName(c);
256
+ const siteName = c.var.appConfig.siteName;
230
257
  const mediaAttachments = await c.var.services.media.getByPostId(post.id);
231
- const r2PublicUrl = c.env.R2_PUBLIC_URL;
232
- const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
233
- const s3PublicUrl = c.env.S3_PUBLIC_URL;
234
- const collections = await c.var.services.collections.list();
258
+ const { r2PublicUrl, imageTransformUrl, s3PublicUrl } = c.var.appConfig;
259
+ const [collections, postCollections] = await Promise.all([
260
+ c.var.services.collections.list(),
261
+ c.var.services.collections.getCollectionsByPostId(post.id),
262
+ ]);
263
+ const postCollectionIds = postCollections.map((c) => c.id);
235
264
 
236
265
  return c.html(
237
266
  <DashLayout
@@ -247,6 +276,7 @@ postsRoutes.get("/:id/edit", async (c) => {
247
276
  imageTransformUrl={imageTransformUrl}
248
277
  s3PublicUrl={s3PublicUrl}
249
278
  collections={collections}
279
+ postCollectionIds={postCollectionIds}
250
280
  />
251
281
  </DashLayout>,
252
282
  );
@@ -257,6 +287,8 @@ postsRoutes.post("/:id", async (c) => {
257
287
  const id = sqid.decode(c.req.param("id"));
258
288
  if (!id) return c.notFound();
259
289
 
290
+ const wantsJson = c.req.header("Accept")?.includes("application/json");
291
+
260
292
  const body = await c.req.json<{
261
293
  format: string;
262
294
  title?: string;
@@ -267,7 +299,7 @@ postsRoutes.post("/:id", async (c) => {
267
299
  url?: string;
268
300
  quoteText?: string;
269
301
  rating?: number;
270
- collectionId?: number;
302
+ collectionIds?: number[];
271
303
  mediaIds?: string[];
272
304
  }>();
273
305
 
@@ -281,7 +313,7 @@ postsRoutes.post("/:id", async (c) => {
281
313
  url: body.url || null,
282
314
  quoteText: body.quoteText || null,
283
315
  rating: body.rating || null,
284
- collectionId: body.collectionId || null,
316
+ collectionIds: body.collectionIds ?? [],
285
317
  });
286
318
 
287
319
  // Update media attachments if provided
@@ -289,7 +321,12 @@ postsRoutes.post("/:id", async (c) => {
289
321
  await c.var.services.media.attachToPost(id, body.mediaIds);
290
322
  }
291
323
 
292
- return dsRedirect(`/dash/posts/${sqid.encode(id)}`);
324
+ const redirectUrl = `/dash/posts/${sqid.encode(id)}`;
325
+ if (wantsJson) {
326
+ return c.json({ status: "redirect" as const, url: redirectUrl });
327
+ }
328
+
329
+ return dsRedirect(redirectUrl);
293
330
  });
294
331
 
295
332
  // Delete post