@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
@@ -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