@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
package/src/lib/view.ts CHANGED
@@ -8,22 +8,21 @@
8
8
  import type {
9
9
  Post,
10
10
  PostWithMedia,
11
- Page,
12
11
  Media,
13
12
  MediaView,
14
13
  PostView,
15
- PageView,
14
+ CollectionTagView,
16
15
  NavItemView,
17
16
  NavItem,
18
17
  SearchResult,
19
18
  SearchResultView,
20
19
  ArchiveGroup,
20
+ Collection,
21
21
  Format,
22
22
  Status,
23
23
  NavItemType,
24
24
  AppConfig,
25
25
  } from "../types.js";
26
- import { encode } from "./sqid.js";
27
26
  import {
28
27
  toISOString,
29
28
  formatDate,
@@ -32,6 +31,9 @@ import {
32
31
  } from "./time.js";
33
32
  import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
34
33
  import { getHtmlExcerpt } from "./excerpt.js";
34
+ import { highlightText } from "./search-snippet.js";
35
+ import { renderCollectionIcon } from "./icons.js";
36
+ import { escapeHtml } from "./html.js";
35
37
 
36
38
  // =============================================================================
37
39
  // Media Context
@@ -78,13 +80,29 @@ export function toMediaView(media: Media, ctx: MediaContext): MediaView {
78
80
  ctx.s3PublicUrl,
79
81
  );
80
82
  const url = getMediaUrl(media.storageKey, publicUrl);
81
- const thumbnailUrl = getImageUrl(url, ctx.imageTransformUrl, {
82
- width: 1200,
83
- height: 768,
84
- quality: 80,
85
- format: "auto",
86
- fit: "scale-down",
87
- });
83
+
84
+ // Only apply image transforms for image MIME types
85
+ const thumbnailUrl = media.mimeType.startsWith("image/")
86
+ ? getImageUrl(url, ctx.imageTransformUrl, {
87
+ width: 1200,
88
+ height: 768,
89
+ quality: 80,
90
+ format: "auto",
91
+ fit: "scale-down",
92
+ })
93
+ : url;
94
+
95
+ const posterRawUrl = media.posterKey
96
+ ? getMediaUrl(media.posterKey, publicUrl)
97
+ : undefined;
98
+ const posterUrl = posterRawUrl
99
+ ? getImageUrl(posterRawUrl, ctx.imageTransformUrl, {
100
+ width: 640,
101
+ quality: 80,
102
+ format: "auto",
103
+ fit: "scale-down",
104
+ })
105
+ : undefined;
88
106
 
89
107
  return {
90
108
  id: media.id,
@@ -95,6 +113,10 @@ export function toMediaView(media: Media, ctx: MediaContext): MediaView {
95
113
  width: media.width ?? undefined,
96
114
  height: media.height ?? undefined,
97
115
  size: media.size,
116
+ blurhash: media.blurhash ?? undefined,
117
+ waveform: media.waveform ?? undefined,
118
+ posterUrl,
119
+ chars: media.chars ?? undefined,
98
120
  };
99
121
  }
100
122
 
@@ -107,10 +129,19 @@ export function toMediaView(media: Media, ctx: MediaContext): MediaView {
107
129
  *
108
130
  * @param post - Post with media attachments from database
109
131
  * @param _ctx - Media context with URL configuration
132
+ * @param postCollections - Optional collections this post belongs to
110
133
  * @returns Render-ready PostView with pre-computed fields
111
134
  */
112
- export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
113
- const permalink = post.path ? `/${post.path}` : `/p/${encode(post.id)}`;
135
+ export function toPostView(
136
+ post: PostWithMedia,
137
+ _ctx: MediaContext,
138
+ postCollections?: Collection[],
139
+ threadRootPermalink?: string,
140
+ isLastInThread?: boolean,
141
+ ): PostView {
142
+ const id = post.id;
143
+ const permalink = `/${post.slug}`;
144
+ const publishedAt = post.publishedAt ?? post.updatedAt;
114
145
 
115
146
  // Pre-compute excerpt from raw body
116
147
  let excerpt: string | undefined;
@@ -124,20 +155,36 @@ export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
124
155
  let summaryHasMore: boolean | undefined;
125
156
  let bodyHtmlWithAnchor = post.bodyHtml;
126
157
  if (post.title && post.bodyHtml) {
127
- const result = getHtmlExcerpt(post.bodyHtml);
128
- summaryHtml = result.excerpt;
129
- summaryHasMore = result.hasMore;
130
-
131
- // Inject #continue anchor at the excerpt boundary for scroll targeting
132
- if (result.hasMore) {
133
- const pos = result.excerptEnd;
134
- bodyHtmlWithAnchor =
135
- post.bodyHtml.slice(0, pos) +
136
- '<span id="continue"></span>' +
137
- post.bodyHtml.slice(pos);
158
+ if (post.summary) {
159
+ // Use stored summary (generated from Tiptap JSON)
160
+ summaryHtml = post.summary
161
+ .split("\n\n")
162
+ .map((p) => `<p>${escapeHtml(p)}</p>`)
163
+ .join("");
164
+ summaryHasMore = true;
165
+ } else {
166
+ // Fallback: extract from rendered HTML
167
+ const result = getHtmlExcerpt(post.bodyHtml);
168
+ summaryHtml = result.excerpt;
169
+ summaryHasMore = result.hasMore;
170
+
171
+ // Inject #continue anchor at the excerpt boundary for scroll targeting
172
+ if (result.hasMore) {
173
+ const pos = result.excerptEnd;
174
+ bodyHtmlWithAnchor =
175
+ post.bodyHtml.slice(0, pos) +
176
+ '<span id="continue"></span>' +
177
+ post.bodyHtml.slice(pos);
178
+ }
138
179
  }
139
180
  }
140
181
 
182
+ // Convert collection tags
183
+ const collections: CollectionTagView[] = (postCollections ?? []).map((c) => {
184
+ const iconHtml = renderCollectionIcon(c.icon, { size: 12 }) || undefined;
185
+ return { slug: c.slug, title: c.title, iconHtml };
186
+ });
187
+
141
188
  // Convert media attachments
142
189
  const media: MediaView[] = post.mediaAttachments.map((m) => ({
143
190
  id: m.id,
@@ -147,12 +194,19 @@ export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
147
194
  altText: m.alt ?? undefined,
148
195
  width: m.width ?? undefined,
149
196
  height: m.height ?? undefined,
197
+ size: m.size ?? undefined,
198
+ blurhash: m.blurhash ?? undefined,
199
+ waveform: m.waveform ?? undefined,
200
+ posterUrl: m.posterUrl ?? undefined,
201
+ originalName: m.originalName ?? undefined,
202
+ summary: m.summary ?? undefined,
203
+ chars: m.chars ?? undefined,
150
204
  }));
151
205
 
152
206
  return {
153
- id: post.id,
207
+ id,
154
208
  permalink,
155
- path: post.path ?? undefined,
209
+ slug: post.slug,
156
210
  title: post.title ?? undefined,
157
211
  bodyHtml: bodyHtmlWithAnchor ?? undefined,
158
212
  excerpt,
@@ -162,36 +216,69 @@ export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
162
216
  quoteText: post.quoteText ?? undefined,
163
217
  format: post.format as Format,
164
218
  status: post.status as Status,
165
- featured: post.featured === 1,
166
- pinned: post.pinned === 1,
219
+ visibility: post.visibility,
220
+ pinned: post.pinnedAt !== null,
221
+ featured: post.featuredAt !== null,
167
222
  rating: post.rating ?? undefined,
168
- publishedAt: toISOString(post.publishedAt),
169
- publishedAtFormatted: formatDate(post.publishedAt),
170
- publishedAtTime: formatTime(post.publishedAt),
171
- publishedAtRelative: formatRelativeTime(post.publishedAt),
223
+ publishedAt: toISOString(publishedAt),
224
+ publishedAtFormatted: formatDate(publishedAt),
225
+ publishedAtTime: formatTime(publishedAt),
226
+ publishedAtRelative: formatRelativeTime(publishedAt),
172
227
  updatedAt: toISOString(post.updatedAt),
173
228
  media,
229
+ collections,
174
230
  replyToId: post.replyToId ?? undefined,
175
- threadRootId: post.threadId ?? undefined,
231
+ threadRootId: post.replyToId ? post.threadId : undefined,
232
+ threadRootPermalink,
233
+ isLastInThread: isLastInThread ?? true,
176
234
  body: post.body ?? undefined,
177
235
  };
178
236
  }
179
237
 
180
238
  /**
181
239
  * Batch converts PostWithMedia[] to PostView[].
240
+ *
241
+ * @param posts - Posts with media attachments
242
+ * @param ctx - Media context with URL configuration
243
+ * @param threadRootPermalinkMap - Optional map of thread root ID → permalink
244
+ * @returns Render-ready PostView[]
182
245
  */
183
246
  export function toPostViews(
184
247
  posts: PostWithMedia[],
185
248
  ctx: MediaContext,
249
+ threadRootPermalinkMap?: Map<string, string>,
250
+ isLastInThreadMap?: Map<string, boolean>,
186
251
  ): PostView[] {
187
- return posts.map((p) => toPostView(p, ctx));
252
+ return posts.map((p) => {
253
+ const rootPermalink = p.replyToId
254
+ ? threadRootPermalinkMap?.get(p.threadId)
255
+ : undefined;
256
+ return toPostView(
257
+ p,
258
+ ctx,
259
+ undefined,
260
+ rootPermalink,
261
+ isLastInThreadMap?.get(p.id),
262
+ );
263
+ });
188
264
  }
189
265
 
190
266
  /**
191
267
  * Converts a bare Post (no media) to a PostView with empty media array.
192
268
  */
193
- export function toPostViewFromPost(post: Post, ctx: MediaContext): PostView {
194
- return toPostView({ ...post, mediaAttachments: [] }, ctx);
269
+ export function toPostViewFromPost(
270
+ post: Post,
271
+ ctx: MediaContext,
272
+ threadRootPermalink?: string,
273
+ isLastInThread?: boolean,
274
+ ): PostView {
275
+ return toPostView(
276
+ { ...post, mediaAttachments: [] },
277
+ ctx,
278
+ undefined,
279
+ threadRootPermalink,
280
+ isLastInThread,
281
+ );
195
282
  }
196
283
 
197
284
  /**
@@ -200,27 +287,54 @@ export function toPostViewFromPost(post: Post, ctx: MediaContext): PostView {
200
287
  export function toPostViewsFromPosts(
201
288
  posts: Post[],
202
289
  ctx: MediaContext,
290
+ threadRootPermalinkMap?: Map<string, string>,
291
+ isLastInThreadMap?: Map<string, boolean>,
203
292
  ): PostView[] {
204
- return posts.map((p) => toPostViewFromPost(p, ctx));
293
+ return posts.map((p) => {
294
+ const rootPermalink = p.replyToId
295
+ ? threadRootPermalinkMap?.get(p.threadId)
296
+ : undefined;
297
+ return toPostViewFromPost(
298
+ p,
299
+ ctx,
300
+ rootPermalink,
301
+ isLastInThreadMap?.get(p.id),
302
+ );
303
+ });
205
304
  }
206
305
 
207
306
  // =============================================================================
208
- // Page Conversions
307
+ // Thread Helpers
209
308
  // =============================================================================
210
309
 
211
310
  /**
212
- * Converts a Page to a render-ready PageView.
311
+ * Builds a map of thread root ID → permalink for posts that are thread replies.
312
+ *
313
+ * @param posts - Posts to inspect for thread membership
314
+ * @param getById - Lookup function to fetch a post by ID
315
+ * @returns Map of thread root ID → permalink string (e.g. `/{slug}`)
316
+ *
317
+ * @example
318
+ * ```ts
319
+ * const map = await loadThreadRootPermalinks(posts, services.posts.getById);
320
+ * const views = toPostViews(postsWithMedia, mediaCtx, map);
321
+ * ```
213
322
  */
214
- export function toPageView(page: Page): PageView {
215
- return {
216
- id: page.id,
217
- slug: page.slug,
218
- title: page.title ?? undefined,
219
- bodyHtml: page.bodyHtml ?? undefined,
220
- status: page.status as Status,
221
- createdAt: toISOString(page.createdAt),
222
- updatedAt: toISOString(page.updatedAt),
223
- };
323
+ export async function loadThreadRootPermalinks(
324
+ posts: Post[],
325
+ getById: (id: string) => Promise<Post | null>,
326
+ ): Promise<Map<string, string>> {
327
+ const threadRootIds = [
328
+ ...new Set(posts.filter((p) => p.replyToId).map((p) => p.threadId)),
329
+ ];
330
+ const map = new Map<string, string>();
331
+ if (threadRootIds.length > 0) {
332
+ const roots = await Promise.all(threadRootIds.map(getById));
333
+ for (const root of roots) {
334
+ if (root) map.set(root.id, `/${root.slug}`);
335
+ }
336
+ }
337
+ return map;
224
338
  }
225
339
 
226
340
  // =============================================================================
@@ -232,7 +346,7 @@ export function toPageView(page: Page): PageView {
232
346
  *
233
347
  * @param item - Raw nav item from database
234
348
  * @param currentPath - Current URL path for active state
235
- * @param isAuthenticated - Whether the user is logged in (affects system dashboard item)
349
+ * @param isAuthenticated - Whether the user is logged in (affects system settings item)
236
350
  */
237
351
  export function toNavItemView(
238
352
  item: NavItem,
@@ -242,9 +356,13 @@ export function toNavItemView(
242
356
  let url = item.url;
243
357
  let label = item.label;
244
358
 
245
- // System dashboard item: resolve URL and label based on auth
246
- if (item.type === "system" && item.url === "/dash") {
247
- url = isAuthenticated ? "/dash" : "/signin";
359
+ // System settings item: resolve URL and label based on auth
360
+ // Also handles legacy "/dash" URLs from existing DB data
361
+ if (
362
+ item.type === "system" &&
363
+ (item.url === "/settings" || item.url === "/dash")
364
+ ) {
365
+ url = isAuthenticated ? "/settings" : "/signin";
248
366
  if (!isAuthenticated) {
249
367
  label = "Sign in";
250
368
  }
@@ -266,7 +384,6 @@ export function toNavItemView(
266
384
  type: item.type as NavItemType,
267
385
  label,
268
386
  url,
269
- pageId: item.pageId ?? undefined,
270
387
  isActive,
271
388
  isExternal,
272
389
  };
@@ -293,26 +410,57 @@ export function toNavItemViews(
293
410
 
294
411
  /**
295
412
  * Converts a SearchResult to a SearchResultView with PostView.
413
+ *
414
+ * @param result - Raw search result with post and FTS metadata
415
+ * @param ctx - Media context for URL computation
416
+ * @param query - Original search query for client-side title/quote highlighting
296
417
  */
297
418
  export function toSearchResultView(
298
419
  result: SearchResult,
299
420
  ctx: MediaContext,
421
+ query?: string,
300
422
  ): SearchResultView {
423
+ const post = toPostViewFromPost(result.post, ctx);
424
+
425
+ let titleHighlighted: string | undefined;
426
+ let quoteHighlighted: string | undefined;
427
+
428
+ if (query) {
429
+ if (post.title) {
430
+ titleHighlighted = highlightText(post.title, query);
431
+ }
432
+ if (post.quoteText) {
433
+ // Truncate before highlighting to avoid splitting inside <mark> tags
434
+ const truncated =
435
+ post.quoteText.length > 120
436
+ ? post.quoteText.slice(0, 120) + "..."
437
+ : post.quoteText;
438
+ quoteHighlighted = highlightText(truncated, query);
439
+ }
440
+ }
441
+
301
442
  return {
302
- post: toPostViewFromPost(result.post, ctx),
443
+ post,
303
444
  rank: result.rank,
304
445
  snippet: result.snippet,
446
+ titleHighlighted,
447
+ quoteHighlighted,
305
448
  };
306
449
  }
307
450
 
308
451
  /**
309
452
  * Batch converts SearchResult[] to SearchResultView[].
453
+ *
454
+ * @param results - Raw search results
455
+ * @param ctx - Media context for URL computation
456
+ * @param query - Original search query for title/quote highlighting
310
457
  */
311
458
  export function toSearchResultViews(
312
459
  results: SearchResult[],
313
460
  ctx: MediaContext,
461
+ query?: string,
314
462
  ): SearchResultView[] {
315
- return results.map((r) => toSearchResultView(r, ctx));
463
+ return results.map((r) => toSearchResultView(r, ctx, query));
316
464
  }
317
465
 
318
466
  // =============================================================================
@@ -346,3 +494,36 @@ export function toArchiveGroups(
346
494
  }
347
495
  return groups;
348
496
  }
497
+
498
+ /**
499
+ * Converts a grouped PostWithMedia map to typed ArchiveGroup[].
500
+ * Unlike toArchiveGroups, this preserves media attachments on each post.
501
+ *
502
+ * @param grouped - Map of "YYYY-MM" keys to PostWithMedia arrays
503
+ * @param ctx - Media context for URL computation
504
+ * @returns ArchiveGroup[] with full media data on each PostView
505
+ */
506
+ export function toArchiveGroupsWithMedia(
507
+ grouped: Map<string, PostWithMedia[]>,
508
+ ctx: MediaContext,
509
+ ): ArchiveGroup[] {
510
+ const groups: ArchiveGroup[] = [];
511
+ for (const [yearMonth, posts] of grouped) {
512
+ const [year, month] = yearMonth.split("-");
513
+ if (!year || !month) continue;
514
+
515
+ const date = new Date(parseInt(year, 10), parseInt(month, 10) - 1);
516
+ const label = date.toLocaleDateString("en-US", {
517
+ year: "numeric",
518
+ month: "long",
519
+ });
520
+
521
+ groups.push({
522
+ year,
523
+ month,
524
+ label,
525
+ posts: toPostViews(posts, ctx),
526
+ });
527
+ }
528
+ return groups;
529
+ }