@jant/core 0.3.35 → 0.3.37

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 (307) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -1,239 +0,0 @@
1
- /**
2
- * Dashboard Pages Routes
3
- *
4
- * Page CRUD management.
5
- */
6
-
7
- import { Hono } from "hono";
8
- import { msg } from "@lingui/core/macro";
9
- import { useLingui } from "@lingui/react/macro";
10
- import type { Bindings, Page } from "../../types.js";
11
- import type { AppVariables } from "../../types/app-context.js";
12
- import { DashLayout } from "../../ui/layouts/DashLayout.js";
13
- import { PageForm, ActionButtons, DangerZone } from "../../ui/dash/index.js";
14
- import { dsRedirect, dsToast } from "../../lib/sse.js";
15
- import { CreatePageSchema } from "../../lib/schemas.js";
16
- import { PagesContent } from "../../ui/dash/pages/PagesContent.js";
17
- import { getI18n } from "../../i18n/index.js";
18
-
19
- type Env = { Bindings: Bindings; Variables: AppVariables };
20
-
21
- export const pagesRoutes = new Hono<Env>();
22
-
23
- // =============================================================================
24
- // Inline components (small, tightly coupled to route params)
25
- // =============================================================================
26
-
27
- function NewPageContent() {
28
- const { t } = useLingui();
29
- return (
30
- <>
31
- <h1 class="text-2xl font-semibold mb-6">
32
- {t({ message: "New Page", comment: "@context: New page main heading" })}
33
- </h1>
34
- <PageForm action="/dash/pages" />
35
- </>
36
- );
37
- }
38
-
39
- function ViewPageContent({ page }: { page: Page }) {
40
- const { t } = useLingui();
41
- return (
42
- <>
43
- <div class="flex items-center justify-between mb-6">
44
- <div>
45
- <h1 class="text-2xl font-semibold">
46
- {page.title ||
47
- t({
48
- message: "Page",
49
- comment: "@context: Default page heading when untitled",
50
- })}
51
- </h1>
52
- <p class="text-muted-foreground mt-1">/{page.slug}</p>
53
- </div>
54
- <ActionButtons
55
- editHref={`/dash/pages/${page.id}/edit`}
56
- editLabel={t({
57
- message: "Edit",
58
- comment: "@context: Button to edit page",
59
- })}
60
- viewHref={page.status !== "draft" ? `/${page.slug}` : undefined}
61
- viewLabel={t({
62
- message: "View",
63
- comment: "@context: Button to view page on public site",
64
- })}
65
- />
66
- </div>
67
-
68
- <div class="card">
69
- <section>
70
- <div
71
- class="prose"
72
- dangerouslySetInnerHTML={{ __html: page.bodyHtml || "" }}
73
- />
74
- </section>
75
- </div>
76
-
77
- <DangerZone
78
- actionLabel={t({
79
- message: "Delete Page",
80
- comment: "@context: Button to delete page",
81
- })}
82
- formAction={`/dash/pages/${page.id}/delete`}
83
- confirmMessage="Are you sure you want to delete this page?"
84
- />
85
- </>
86
- );
87
- }
88
-
89
- function EditPageContent({ page }: { page: Page }) {
90
- const { t } = useLingui();
91
- return (
92
- <>
93
- <h1 class="text-2xl font-semibold mb-6">
94
- {t({
95
- message: "Edit Page",
96
- comment: "@context: Edit page main heading",
97
- })}
98
- </h1>
99
- <PageForm page={page} action={`/dash/pages/${page.id}`} />
100
- </>
101
- );
102
- }
103
-
104
- // =============================================================================
105
- // Route handlers
106
- // =============================================================================
107
-
108
- pagesRoutes.get("/", async (c) => {
109
- const pages = await c.var.services.pages.list();
110
- const siteName = c.var.appConfig.siteName;
111
-
112
- return c.html(
113
- <DashLayout
114
- c={c}
115
- title="Pages"
116
- siteName={siteName}
117
- currentPath="/dash/pages"
118
- >
119
- <PagesContent pages={pages} />
120
- </DashLayout>,
121
- );
122
- });
123
-
124
- pagesRoutes.get("/new", async (c) => {
125
- const siteName = c.var.appConfig.siteName;
126
- return c.html(
127
- <DashLayout
128
- c={c}
129
- title="New Page"
130
- siteName={siteName}
131
- currentPath="/dash/pages"
132
- >
133
- <NewPageContent />
134
- </DashLayout>,
135
- );
136
- });
137
-
138
- pagesRoutes.post("/", async (c) => {
139
- const i18n = getI18n(c);
140
- const raw = await c.req.json();
141
- const parsed = CreatePageSchema.safeParse(raw);
142
- if (!parsed.success) {
143
- const errorMsg =
144
- parsed.error.issues[0]?.message ??
145
- i18n._(
146
- msg({
147
- message: "Invalid input",
148
- comment: "@context: Fallback validation error for page form",
149
- }),
150
- );
151
- return dsToast(errorMsg, "error");
152
- }
153
-
154
- const page = await c.var.services.pages.create({
155
- title: parsed.data.title,
156
- body: parsed.data.body,
157
- status: parsed.data.status,
158
- slug: parsed.data.slug,
159
- });
160
-
161
- return dsRedirect(`/dash/pages/${page.id}`);
162
- });
163
-
164
- pagesRoutes.get("/:id", async (c) => {
165
- const id = parseInt(c.req.param("id"), 10);
166
- if (isNaN(id)) return c.notFound();
167
-
168
- const page = await c.var.services.pages.getById(id);
169
- if (!page) return c.notFound();
170
-
171
- const siteName = c.var.appConfig.siteName;
172
- return c.html(
173
- <DashLayout
174
- c={c}
175
- title={page.title || "Page"}
176
- siteName={siteName}
177
- currentPath="/dash/pages"
178
- >
179
- <ViewPageContent page={page} />
180
- </DashLayout>,
181
- );
182
- });
183
-
184
- pagesRoutes.get("/:id/edit", async (c) => {
185
- const id = parseInt(c.req.param("id"), 10);
186
- if (isNaN(id)) return c.notFound();
187
-
188
- const page = await c.var.services.pages.getById(id);
189
- if (!page) return c.notFound();
190
-
191
- const siteName = c.var.appConfig.siteName;
192
- return c.html(
193
- <DashLayout
194
- c={c}
195
- title={`Edit: ${page.title || "Page"}`}
196
- siteName={siteName}
197
- currentPath="/dash/pages"
198
- >
199
- <EditPageContent page={page} />
200
- </DashLayout>,
201
- );
202
- });
203
-
204
- pagesRoutes.post("/:id", async (c) => {
205
- const i18n = getI18n(c);
206
- const id = parseInt(c.req.param("id"), 10);
207
- if (isNaN(id)) return c.notFound();
208
-
209
- const raw = await c.req.json();
210
- const parsed = CreatePageSchema.safeParse(raw);
211
- if (!parsed.success) {
212
- const errorMsg =
213
- parsed.error.issues[0]?.message ??
214
- i18n._(
215
- msg({
216
- message: "Invalid input",
217
- comment: "@context: Fallback validation error for page form",
218
- }),
219
- );
220
- return dsToast(errorMsg, "error");
221
- }
222
-
223
- await c.var.services.pages.update(id, {
224
- title: parsed.data.title,
225
- body: parsed.data.body,
226
- status: parsed.data.status,
227
- slug: parsed.data.slug,
228
- });
229
-
230
- return dsRedirect(`/dash/pages/${id}`);
231
- });
232
-
233
- pagesRoutes.post("/:id/delete", async (c) => {
234
- const id = parseInt(c.req.param("id"), 10);
235
- if (isNaN(id)) return c.notFound();
236
-
237
- await c.var.services.pages.delete(id);
238
- return dsRedirect("/dash/pages");
239
- });
@@ -1,334 +0,0 @@
1
- /**
2
- * Dashboard Posts Routes
3
- */
4
-
5
- import { Hono } from "hono";
6
- import { useLingui } from "@lingui/react/macro";
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";
15
- import { DashLayout } from "../../ui/layouts/DashLayout.js";
16
- import {
17
- PostForm,
18
- PostList,
19
- CrudPageHeader,
20
- ActionButtons,
21
- } from "../../ui/dash/index.js";
22
- import * as sqid from "../../lib/sqid.js";
23
- import { dsRedirect } from "../../lib/sse.js";
24
- import {
25
- CreatePostSchema,
26
- UpdatePostSchema,
27
- parseValidated,
28
- } from "../../lib/schemas.js";
29
- import {
30
- toPostViewsFromPosts,
31
- toPostViewFromPost,
32
- createMediaContext,
33
- } from "../../lib/view.js";
34
-
35
- type Env = { Bindings: Bindings; Variables: AppVariables };
36
-
37
- export const postsRoutes = new Hono<Env>();
38
-
39
- function PostsListContent({ posts }: { posts: PostView[] }) {
40
- const { t } = useLingui();
41
- return (
42
- <>
43
- <CrudPageHeader
44
- title={t({ message: "Posts", comment: "@context: Dashboard heading" })}
45
- ctaLabel={t({
46
- message: "New Post",
47
- comment: "@context: Button to create new post",
48
- })}
49
- ctaHref="/dash/posts/new"
50
- />
51
- <PostList posts={posts} />
52
- </>
53
- );
54
- }
55
-
56
- function NewPostContent({ collections }: { collections: Collection[] }) {
57
- const { t } = useLingui();
58
- return (
59
- <>
60
- <h1 class="text-2xl font-semibold mb-6">
61
- {t({ message: "New Post", comment: "@context: Page heading" })}
62
- </h1>
63
- <PostForm action="/dash/posts" collections={collections} />
64
- </>
65
- );
66
- }
67
-
68
- // List posts
69
- postsRoutes.get("/", async (c) => {
70
- const posts = await c.var.services.posts.list({
71
- excludeReplies: true,
72
- });
73
- const siteName = c.var.appConfig.siteName;
74
- const postViews = toPostViewsFromPosts(
75
- posts,
76
- createMediaContext(c.var.appConfig),
77
- );
78
-
79
- return c.html(
80
- <DashLayout
81
- c={c}
82
- title="Posts"
83
- siteName={siteName}
84
- currentPath="/dash/posts"
85
- >
86
- <PostsListContent posts={postViews} />
87
- </DashLayout>,
88
- );
89
- });
90
-
91
- // New post form
92
- postsRoutes.get("/new", async (c) => {
93
- const siteName = c.var.appConfig.siteName;
94
- const collections = await c.var.services.collections.list();
95
-
96
- return c.html(
97
- <DashLayout
98
- c={c}
99
- title="New Post"
100
- siteName={siteName}
101
- currentPath="/dash/posts"
102
- >
103
- <NewPostContent collections={collections} />
104
- </DashLayout>,
105
- );
106
- });
107
-
108
- // Create post
109
- postsRoutes.post("/", async (c) => {
110
- const wantsJson = c.req.header("Accept")?.includes("application/json");
111
- const body = parseValidated(CreatePostSchema, await c.req.json());
112
-
113
- // Validate media IDs before creating post
114
- if (body.mediaIds && body.mediaIds.length > 0) {
115
- await c.var.services.media.validateIds(body.mediaIds);
116
- }
117
-
118
- const post = await c.var.services.posts.create({
119
- format: body.format,
120
- title: body.title || undefined,
121
- body: body.body,
122
- status: body.status,
123
- featured: body.featured,
124
- pinned: body.pinned,
125
- url: body.url || undefined,
126
- quoteText: body.quoteText || undefined,
127
- rating: body.rating || undefined,
128
- collectionIds: body.collectionIds?.length ? body.collectionIds : undefined,
129
- });
130
-
131
- // Attach media if provided
132
- if (body.mediaIds && body.mediaIds.length > 0) {
133
- await c.var.services.media.attachToPost(post.id, body.mediaIds);
134
- }
135
-
136
- const redirectUrl = `/dash/posts/${sqid.encode(post.id)}`;
137
- if (wantsJson) {
138
- return c.json({ status: "redirect" as const, url: redirectUrl });
139
- }
140
-
141
- return dsRedirect(redirectUrl);
142
- });
143
-
144
- function ViewPostContent({ post }: { post: PostView }) {
145
- const { t } = useLingui();
146
- const defaultTitle = t({
147
- message: "Post",
148
- comment: "@context: Default post title",
149
- });
150
-
151
- return (
152
- <>
153
- <div class="flex items-center justify-between mb-6">
154
- <h1 class="text-2xl font-semibold">{post.title || defaultTitle}</h1>
155
- <ActionButtons
156
- editHref={`/dash/posts/${sqid.encode(post.id)}/edit`}
157
- editLabel={t({
158
- message: "Edit",
159
- comment: "@context: Button to edit post",
160
- })}
161
- viewHref={post.permalink}
162
- viewLabel={t({
163
- message: "View",
164
- comment: "@context: Button to view post",
165
- })}
166
- />
167
- </div>
168
-
169
- <div class="card">
170
- <section>
171
- <div
172
- class="prose"
173
- dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
174
- />
175
- </section>
176
- </div>
177
- </>
178
- );
179
- }
180
-
181
- function EditPostContent({
182
- post,
183
- mediaAttachments,
184
- r2PublicUrl,
185
- imageTransformUrl,
186
- s3PublicUrl,
187
- collections,
188
- postCollectionIds,
189
- }: {
190
- post: Post;
191
- mediaAttachments: Media[];
192
- r2PublicUrl?: string;
193
- imageTransformUrl?: string;
194
- s3PublicUrl?: string;
195
- collections: Collection[];
196
- postCollectionIds: number[];
197
- }) {
198
- const { t } = useLingui();
199
- return (
200
- <>
201
- <h1 class="text-2xl font-semibold mb-6">
202
- {t({ message: "Edit Post", comment: "@context: Page heading" })}
203
- </h1>
204
- <PostForm
205
- post={post}
206
- action={`/dash/posts/${sqid.encode(post.id)}`}
207
- mediaAttachments={mediaAttachments}
208
- r2PublicUrl={r2PublicUrl}
209
- imageTransformUrl={imageTransformUrl}
210
- s3PublicUrl={s3PublicUrl}
211
- collections={collections}
212
- postCollectionIds={postCollectionIds}
213
- cancelHref={`/dash/posts/${sqid.encode(post.id)}`}
214
- />
215
- </>
216
- );
217
- }
218
-
219
- // View single post
220
- postsRoutes.get("/:id", async (c) => {
221
- const id = sqid.decode(c.req.param("id"));
222
- if (!id) return c.notFound();
223
-
224
- const post = await c.var.services.posts.getById(id);
225
- if (!post) return c.notFound();
226
-
227
- const siteName = c.var.appConfig.siteName;
228
- const pageTitle = post.title || "Post";
229
- const postView = toPostViewFromPost(
230
- post,
231
- createMediaContext(c.var.appConfig),
232
- );
233
-
234
- return c.html(
235
- <DashLayout
236
- c={c}
237
- title={pageTitle}
238
- siteName={siteName}
239
- currentPath="/dash/posts"
240
- >
241
- <ViewPostContent post={postView} />
242
- </DashLayout>,
243
- );
244
- });
245
-
246
- // Edit post form
247
- postsRoutes.get("/:id/edit", async (c) => {
248
- const id = sqid.decode(c.req.param("id"));
249
- if (!id) return c.notFound();
250
-
251
- const post = await c.var.services.posts.getById(id);
252
- if (!post) return c.notFound();
253
-
254
- const siteName = c.var.appConfig.siteName;
255
- const mediaAttachments = await c.var.services.media.getByPostId(post.id);
256
- const { r2PublicUrl, imageTransformUrl, s3PublicUrl } = c.var.appConfig;
257
- const [collections, postCollections] = await Promise.all([
258
- c.var.services.collections.list(),
259
- c.var.services.collections.getCollectionsByPostId(post.id),
260
- ]);
261
- const postCollectionIds = postCollections.map((c) => c.id);
262
-
263
- return c.html(
264
- <DashLayout
265
- c={c}
266
- title={`Edit: ${post.title || "Post"}`}
267
- siteName={siteName}
268
- currentPath="/dash/posts"
269
- >
270
- <EditPostContent
271
- post={post}
272
- mediaAttachments={mediaAttachments}
273
- r2PublicUrl={r2PublicUrl}
274
- imageTransformUrl={imageTransformUrl}
275
- s3PublicUrl={s3PublicUrl}
276
- collections={collections}
277
- postCollectionIds={postCollectionIds}
278
- />
279
- </DashLayout>,
280
- );
281
- });
282
-
283
- // Update post
284
- postsRoutes.post("/:id", async (c) => {
285
- const id = sqid.decode(c.req.param("id"));
286
- if (!id) return c.notFound();
287
-
288
- const wantsJson = c.req.header("Accept")?.includes("application/json");
289
-
290
- const body = parseValidated(UpdatePostSchema, await c.req.json());
291
-
292
- // Validate media IDs if provided
293
- if (body.mediaIds !== undefined) {
294
- await c.var.services.media.validateIds(body.mediaIds);
295
- }
296
-
297
- await c.var.services.posts.update(id, {
298
- format: body.format,
299
- title: body.title || null,
300
- body: body.body || null,
301
- status: body.status,
302
- featured: body.featured,
303
- pinned: body.pinned,
304
- url: body.url || null,
305
- quoteText: body.quoteText || null,
306
- rating: body.rating || null,
307
- collectionIds: body.collectionIds ?? [],
308
- });
309
-
310
- // Update media attachments if provided
311
- if (body.mediaIds !== undefined) {
312
- await c.var.services.media.attachToPost(id, body.mediaIds);
313
- }
314
-
315
- const redirectUrl = `/dash/posts/${sqid.encode(id)}`;
316
- if (wantsJson) {
317
- return c.json({ status: "redirect" as const, url: redirectUrl });
318
- }
319
-
320
- return dsRedirect(redirectUrl);
321
- });
322
-
323
- // Delete post
324
- postsRoutes.post("/:id/delete", async (c) => {
325
- const id = sqid.decode(c.req.param("id"));
326
- if (!id) return c.notFound();
327
-
328
- await c.var.services.posts.delete(id, {
329
- media: c.var.services.media,
330
- storage: c.var.storage,
331
- });
332
-
333
- return dsRedirect("/dash/posts");
334
- });