@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.
- package/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -140
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +57 -31
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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(
|
|
113
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
207
|
+
id,
|
|
154
208
|
permalink,
|
|
155
|
-
|
|
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
|
-
|
|
166
|
-
pinned: post.
|
|
219
|
+
visibility: post.visibility,
|
|
220
|
+
pinned: post.pinnedAt !== null,
|
|
221
|
+
featured: post.featuredAt !== null,
|
|
167
222
|
rating: post.rating ?? undefined,
|
|
168
|
-
publishedAt: toISOString(
|
|
169
|
-
publishedAtFormatted: formatDate(
|
|
170
|
-
publishedAtTime: formatTime(
|
|
171
|
-
publishedAtRelative: formatRelativeTime(
|
|
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
|
|
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) =>
|
|
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(
|
|
194
|
-
|
|
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) =>
|
|
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
|
-
//
|
|
307
|
+
// Thread Helpers
|
|
209
308
|
// =============================================================================
|
|
210
309
|
|
|
211
310
|
/**
|
|
212
|
-
*
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
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
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
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
|
+
}
|