@jant/core 0.3.23 → 0.3.25
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/dist/app.js +50 -26
- package/dist/db/schema.js +72 -47
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +5 -11
- package/dist/lib/constants.js +2 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +30 -6
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme.js +4 -4
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +95 -0
- package/dist/lib/view.js +61 -72
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +27 -33
- package/dist/routes/api/search.js +4 -5
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -42
- package/dist/routes/dash/index.js +3 -3
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +440 -106
- package/dist/routes/dash/posts.js +27 -37
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +4 -6
- package/dist/routes/feed/sitemap.js +11 -8
- package/dist/routes/pages/archive.js +13 -15
- package/dist/routes/pages/collection.js +12 -9
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +19 -68
- package/dist/routes/pages/page.js +57 -29
- package/dist/routes/pages/post.js +7 -17
- package/dist/routes/pages/search.js +5 -9
- package/dist/services/collection.js +52 -64
- package/dist/services/index.js +5 -3
- package/dist/services/navigation.js +29 -53
- package/dist/services/page.js +84 -0
- package/dist/services/post.js +102 -69
- package/dist/services/search.js +24 -18
- package/dist/types.js +24 -40
- package/dist/ui/compose/ComposeDialog.js +452 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
- package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
- package/dist/{theme/components → ui/dash}/PostList.js +18 -13
- package/dist/ui/dash/StatusBadge.js +46 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/feed/LinkCard.js +72 -0
- package/dist/ui/feed/NoteCard.js +58 -0
- package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
- package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +141 -0
- package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
- package/dist/ui/pages/CollectionPage.js +70 -0
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
- package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
- package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
- package/dist/ui/shared/MediaGallery.js +35 -0
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
- package/dist/ui/shared/index.js +5 -0
- package/package.json +2 -9
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +53 -73
- package/src/app.tsx +56 -28
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +443 -240
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +443 -240
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +443 -240
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +29 -42
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +201 -99
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
- package/src/lib/__tests__/view.test.ts +204 -50
- package/src/lib/constants.ts +2 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +45 -8
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +119 -51
- package/src/lib/theme.ts +5 -5
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +141 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +46 -0
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -0
- package/src/routes/api/__tests__/posts.test.ts +50 -108
- package/src/routes/api/__tests__/search.test.ts +2 -3
- package/src/routes/api/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +28 -28
- package/src/routes/api/search.ts +3 -3
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +20 -42
- package/src/routes/dash/index.tsx +3 -3
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +480 -122
- package/src/routes/dash/posts.tsx +42 -54
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +4 -3
- package/src/routes/feed/sitemap.ts +15 -5
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +15 -15
- package/src/routes/pages/collection.tsx +16 -9
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +21 -92
- package/src/routes/pages/page.tsx +62 -27
- package/src/routes/pages/post.tsx +6 -18
- package/src/routes/pages/search.tsx +3 -7
- package/src/services/__tests__/collection.test.ts +257 -158
- package/src/services/__tests__/media.test.ts +18 -18
- package/src/services/__tests__/navigation.test.ts +161 -87
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +432 -197
- package/src/services/__tests__/search.test.ts +19 -25
- package/src/services/collection.ts +71 -113
- package/src/services/index.ts +9 -8
- package/src/services/navigation.ts +38 -71
- package/src/services/page.ts +136 -0
- package/src/services/post.ts +141 -101
- package/src/services/search.ts +38 -27
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +212 -198
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/ui/dash/FormatBadge.tsx +28 -0
- package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
- package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
- package/src/ui/dash/PostList.tsx +101 -0
- package/src/ui/dash/StatusBadge.tsx +61 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/feed/LinkCard.tsx +72 -0
- package/src/ui/feed/NoteCard.tsx +63 -0
- package/src/ui/feed/QuoteCard.tsx +68 -0
- package/src/ui/feed/ThreadPreview.tsx +48 -0
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +150 -0
- package/src/ui/pages/ArchivePage.tsx +162 -0
- package/src/ui/pages/CollectionPage.tsx +70 -0
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/ui/pages/HomePage.tsx +37 -0
- package/src/ui/pages/PostPage.tsx +56 -0
- package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
- package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
- package/src/ui/shared/MediaGallery.tsx +59 -0
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -49
- package/dist/routes/api/timeline.js +0 -120
- package/dist/routes/dash/navigation.js +0 -288
- package/dist/theme/components/MediaGallery.js +0 -107
- package/dist/theme/components/VisibilityBadge.js +0 -37
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
- package/dist/themes/minimal/index.js +0 -65
- package/dist/themes/minimal/pages/CollectionPage.js +0 -65
- package/dist/themes/minimal/pages/HomePage.js +0 -25
- package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
- package/dist/themes/minimal/timeline/ImageCard.js +0 -67
- package/dist/themes/minimal/timeline/LinkCard.js +0 -47
- package/dist/themes/minimal/timeline/NoteCard.js +0 -34
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
- package/src/lib/__tests__/theme-components.test.ts +0 -126
- package/src/lib/theme-components.ts +0 -68
- package/src/routes/api/timeline.tsx +0 -159
- package/src/routes/dash/navigation.tsx +0 -316
- package/src/theme/components/MediaGallery.tsx +0 -128
- package/src/theme/components/PostList.tsx +0 -92
- package/src/theme/components/TypeBadge.tsx +0 -37
- package/src/theme/components/VisibilityBadge.tsx +0 -45
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/index.ts +0 -83
- package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- package/src/themes/minimal/pages/HomePage.tsx +0 -41
- package/src/themes/minimal/pages/PostPage.tsx +0 -43
- package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
- package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
- package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
- package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
- package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
- package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
- package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
- package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
- /package/dist/{theme → ui}/color-themes.js +0 -0
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme → ui}/color-themes.ts +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Timeline
|
|
2
|
+
* Timeline Data Assembly Tests
|
|
3
3
|
*
|
|
4
4
|
* Tests the timeline data assembly logic via the service layer.
|
|
5
5
|
* The actual route handler renders JSX components which require the Lingui SWC
|
|
6
6
|
* plugin (not available in vitest). We test the underlying service operations
|
|
7
|
-
* that power the timeline
|
|
7
|
+
* that power the timeline instead.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
11
|
-
import { createTestDatabase } from "
|
|
12
|
-
import { createPostService } from "
|
|
13
|
-
import { createMediaService } from "
|
|
14
|
-
import { buildMediaMap } from "
|
|
15
|
-
import type { Database } from "
|
|
16
|
-
import type { PostWithMedia } from "
|
|
11
|
+
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
12
|
+
import { createPostService } from "../../services/post.js";
|
|
13
|
+
import { createMediaService } from "../../services/media.js";
|
|
14
|
+
import { buildMediaMap } from "../media-helpers.js";
|
|
15
|
+
import type { Database } from "../../db/index.js";
|
|
16
|
+
import type { PostWithMedia } from "../../types.js";
|
|
17
17
|
|
|
18
18
|
describe("Timeline data assembly", () => {
|
|
19
19
|
let db: Database;
|
|
@@ -29,15 +29,13 @@ describe("Timeline data assembly", () => {
|
|
|
29
29
|
|
|
30
30
|
it("assembles timeline items with media attachments", async () => {
|
|
31
31
|
const post = await postService.create({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
visibility: "featured",
|
|
32
|
+
format: "note",
|
|
33
|
+
body: "Hello",
|
|
35
34
|
});
|
|
36
35
|
|
|
37
36
|
const posts = await postService.list({
|
|
38
|
-
|
|
37
|
+
status: "published",
|
|
39
38
|
excludeReplies: true,
|
|
40
|
-
excludeTypes: ["page"],
|
|
41
39
|
limit: 21,
|
|
42
40
|
});
|
|
43
41
|
|
|
@@ -60,25 +58,23 @@ describe("Timeline data assembly", () => {
|
|
|
60
58
|
|
|
61
59
|
it("identifies thread roots and builds thread previews", async () => {
|
|
62
60
|
const root = await postService.create({
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
visibility: "featured",
|
|
61
|
+
format: "note",
|
|
62
|
+
body: "Thread root",
|
|
66
63
|
});
|
|
67
64
|
await postService.create({
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
format: "note",
|
|
66
|
+
body: "Reply 1",
|
|
70
67
|
replyToId: root.id,
|
|
71
68
|
});
|
|
72
69
|
await postService.create({
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
format: "note",
|
|
71
|
+
body: "Reply 2",
|
|
75
72
|
replyToId: root.id,
|
|
76
73
|
});
|
|
77
74
|
|
|
78
75
|
const posts = await postService.list({
|
|
79
|
-
|
|
76
|
+
status: "published",
|
|
80
77
|
excludeReplies: true,
|
|
81
|
-
excludeTypes: ["page"],
|
|
82
78
|
limit: 21,
|
|
83
79
|
});
|
|
84
80
|
|
|
@@ -96,7 +92,7 @@ describe("Timeline data assembly", () => {
|
|
|
96
92
|
const threadPreviews = await postService.getThreadPreviews(threadRootIds);
|
|
97
93
|
const replies = threadPreviews.get(root.id);
|
|
98
94
|
expect(replies).toHaveLength(2);
|
|
99
|
-
expect(replies?.[0]?.
|
|
95
|
+
expect(replies?.[0]?.body).toBe("Reply 1");
|
|
100
96
|
|
|
101
97
|
// Assemble items
|
|
102
98
|
const rawMediaMap = await mediaService.getByPostIds(postIds);
|
|
@@ -133,50 +129,25 @@ describe("Timeline data assembly", () => {
|
|
|
133
129
|
expect(items[0]?.threadPreview?.totalReplyCount).toBe(2);
|
|
134
130
|
});
|
|
135
131
|
|
|
136
|
-
it("excludes pages from timeline", async () => {
|
|
137
|
-
await postService.create({
|
|
138
|
-
type: "note",
|
|
139
|
-
content: "A note",
|
|
140
|
-
visibility: "quiet",
|
|
141
|
-
});
|
|
142
|
-
await postService.create({
|
|
143
|
-
type: "page",
|
|
144
|
-
content: "A page",
|
|
145
|
-
visibility: "quiet",
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const posts = await postService.list({
|
|
149
|
-
visibility: ["featured", "quiet"],
|
|
150
|
-
excludeReplies: true,
|
|
151
|
-
excludeTypes: ["page"],
|
|
152
|
-
limit: 21,
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
expect(posts).toHaveLength(1);
|
|
156
|
-
expect(posts[0]?.type).toBe("note");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
132
|
it("excludes replies from top-level list", async () => {
|
|
160
133
|
const root = await postService.create({
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
visibility: "quiet",
|
|
134
|
+
format: "note",
|
|
135
|
+
body: "Root",
|
|
164
136
|
});
|
|
165
137
|
await postService.create({
|
|
166
|
-
|
|
167
|
-
|
|
138
|
+
format: "note",
|
|
139
|
+
body: "Reply",
|
|
168
140
|
replyToId: root.id,
|
|
169
141
|
});
|
|
170
142
|
|
|
171
143
|
const posts = await postService.list({
|
|
172
|
-
|
|
144
|
+
status: "published",
|
|
173
145
|
excludeReplies: true,
|
|
174
|
-
excludeTypes: ["page"],
|
|
175
146
|
limit: 21,
|
|
176
147
|
});
|
|
177
148
|
|
|
178
149
|
expect(posts).toHaveLength(1);
|
|
179
|
-
expect(posts[0]?.
|
|
150
|
+
expect(posts[0]?.body).toBe("Root");
|
|
180
151
|
});
|
|
181
152
|
|
|
182
153
|
it("supports cursor pagination for load more", async () => {
|
|
@@ -184,9 +155,8 @@ describe("Timeline data assembly", () => {
|
|
|
184
155
|
for (let i = 0; i < 5; i++) {
|
|
185
156
|
posts.push(
|
|
186
157
|
await postService.create({
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
visibility: "quiet",
|
|
158
|
+
format: "note",
|
|
159
|
+
body: `Post ${i}`,
|
|
190
160
|
publishedAt: 1000 + i,
|
|
191
161
|
}),
|
|
192
162
|
);
|
|
@@ -194,9 +164,8 @@ describe("Timeline data assembly", () => {
|
|
|
194
164
|
|
|
195
165
|
// First page
|
|
196
166
|
const page1 = await postService.list({
|
|
197
|
-
|
|
167
|
+
status: "published",
|
|
198
168
|
excludeReplies: true,
|
|
199
|
-
excludeTypes: ["page"],
|
|
200
169
|
limit: 3,
|
|
201
170
|
});
|
|
202
171
|
expect(page1).toHaveLength(3);
|
|
@@ -205,9 +174,8 @@ describe("Timeline data assembly", () => {
|
|
|
205
174
|
const lastPost = page1[page1.length - 1];
|
|
206
175
|
expect(lastPost).toBeDefined();
|
|
207
176
|
const page2 = await postService.list({
|
|
208
|
-
|
|
177
|
+
status: "published",
|
|
209
178
|
excludeReplies: true,
|
|
210
|
-
excludeTypes: ["page"],
|
|
211
179
|
limit: 3,
|
|
212
180
|
cursor: lastPost?.id,
|
|
213
181
|
});
|
|
@@ -215,28 +183,66 @@ describe("Timeline data assembly", () => {
|
|
|
215
183
|
expect(page2.every((p) => p.id < (lastPost?.id ?? 0))).toBe(true);
|
|
216
184
|
});
|
|
217
185
|
|
|
218
|
-
it("
|
|
219
|
-
for (let i = 0; i <
|
|
186
|
+
it("supports offset-based pagination for page navigation", async () => {
|
|
187
|
+
for (let i = 0; i < 5; i++) {
|
|
220
188
|
await postService.create({
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
189
|
+
format: "note",
|
|
190
|
+
body: `Post ${i}`,
|
|
191
|
+
publishedAt: 1000 + i,
|
|
224
192
|
});
|
|
225
193
|
}
|
|
226
194
|
|
|
227
|
-
// Request limit + 1 to check for more
|
|
228
195
|
const pageSize = 2;
|
|
229
|
-
|
|
230
|
-
|
|
196
|
+
|
|
197
|
+
// Page 1
|
|
198
|
+
const page1 = await postService.list({
|
|
199
|
+
status: "published",
|
|
231
200
|
excludeReplies: true,
|
|
232
|
-
|
|
233
|
-
|
|
201
|
+
limit: pageSize,
|
|
202
|
+
offset: 0,
|
|
234
203
|
});
|
|
204
|
+
expect(page1).toHaveLength(2);
|
|
205
|
+
expect(page1[0]?.body).toBe("Post 4");
|
|
206
|
+
expect(page1[1]?.body).toBe("Post 3");
|
|
235
207
|
|
|
236
|
-
|
|
237
|
-
|
|
208
|
+
// Page 2
|
|
209
|
+
const page2 = await postService.list({
|
|
210
|
+
status: "published",
|
|
211
|
+
excludeReplies: true,
|
|
212
|
+
limit: pageSize,
|
|
213
|
+
offset: 2,
|
|
214
|
+
});
|
|
215
|
+
expect(page2).toHaveLength(2);
|
|
216
|
+
expect(page2[0]?.body).toBe("Post 2");
|
|
217
|
+
expect(page2[1]?.body).toBe("Post 1");
|
|
218
|
+
|
|
219
|
+
// Page 3 (partial)
|
|
220
|
+
const page3 = await postService.list({
|
|
221
|
+
status: "published",
|
|
222
|
+
excludeReplies: true,
|
|
223
|
+
limit: pageSize,
|
|
224
|
+
offset: 4,
|
|
225
|
+
});
|
|
226
|
+
expect(page3).toHaveLength(1);
|
|
227
|
+
expect(page3[0]?.body).toBe("Post 0");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("computes total pages from count", async () => {
|
|
231
|
+
for (let i = 0; i < 5; i++) {
|
|
232
|
+
await postService.create({
|
|
233
|
+
format: "note",
|
|
234
|
+
body: `Post ${i}`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const pageSize = 2;
|
|
239
|
+
const totalCount = await postService.count({
|
|
240
|
+
status: "published",
|
|
241
|
+
excludeReplies: true,
|
|
242
|
+
});
|
|
238
243
|
|
|
239
|
-
|
|
240
|
-
|
|
244
|
+
expect(totalCount).toBe(5);
|
|
245
|
+
const totalPages = Math.ceil(totalCount / pageSize);
|
|
246
|
+
expect(totalPages).toBe(3);
|
|
241
247
|
});
|
|
242
248
|
});
|
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
toPostView,
|
|
8
8
|
toPostViews,
|
|
9
9
|
toMediaView,
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
toNavItemView,
|
|
11
|
+
toNavItemViews,
|
|
12
12
|
toSearchResultView,
|
|
13
13
|
toArchiveGroups,
|
|
14
14
|
} from "../view.js";
|
|
@@ -16,7 +16,7 @@ import type { MediaContext } from "../view.js";
|
|
|
16
16
|
import type {
|
|
17
17
|
PostWithMedia,
|
|
18
18
|
Media,
|
|
19
|
-
|
|
19
|
+
NavItem,
|
|
20
20
|
SearchResult,
|
|
21
21
|
Post,
|
|
22
22
|
} from "../../types.js";
|
|
@@ -30,15 +30,18 @@ const CTX_WITH_URLS: MediaContext = {
|
|
|
30
30
|
function makePost(overrides: Partial<Post> = {}): Post {
|
|
31
31
|
return {
|
|
32
32
|
id: 1,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
format: "note",
|
|
34
|
+
status: "published",
|
|
35
|
+
featured: 0,
|
|
36
|
+
pinned: 0,
|
|
36
37
|
path: null,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
title: null,
|
|
39
|
+
url: null,
|
|
40
|
+
body: "Hello world",
|
|
41
|
+
bodyHtml: "<p>Hello world</p>",
|
|
42
|
+
quoteText: null,
|
|
43
|
+
rating: null,
|
|
44
|
+
collectionId: null,
|
|
42
45
|
replyToId: null,
|
|
43
46
|
threadId: null,
|
|
44
47
|
deletedAt: null,
|
|
@@ -78,11 +81,13 @@ function makeMedia(overrides: Partial<Media> = {}): Media {
|
|
|
78
81
|
};
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
function
|
|
84
|
+
function makeNavItem(overrides: Partial<NavItem> = {}): NavItem {
|
|
82
85
|
return {
|
|
83
86
|
id: 1,
|
|
87
|
+
type: "link",
|
|
84
88
|
label: "Home",
|
|
85
89
|
url: "/",
|
|
90
|
+
pageId: null,
|
|
86
91
|
position: 0,
|
|
87
92
|
createdAt: 1706745600,
|
|
88
93
|
updatedAt: 1706745600,
|
|
@@ -95,13 +100,25 @@ function makeNavLink(overrides: Partial<NavigationLink> = {}): NavigationLink {
|
|
|
95
100
|
// =============================================================================
|
|
96
101
|
|
|
97
102
|
describe("toPostView", () => {
|
|
98
|
-
it("generates permalink from post id", () => {
|
|
99
|
-
const post = makePostWithMedia({ id: 123 });
|
|
103
|
+
it("generates permalink from post id when no path", () => {
|
|
104
|
+
const post = makePostWithMedia({ id: 123, path: null });
|
|
100
105
|
const view = toPostView(post, EMPTY_CTX);
|
|
101
106
|
expect(view.permalink).toMatch(/^\/p\/.+$/);
|
|
102
107
|
expect(view.permalink.length).toBeGreaterThan(3);
|
|
103
108
|
});
|
|
104
109
|
|
|
110
|
+
it("generates permalink from path when path is set", () => {
|
|
111
|
+
const post = makePostWithMedia({ id: 123, path: "my-post" });
|
|
112
|
+
const view = toPostView(post, EMPTY_CTX);
|
|
113
|
+
expect(view.permalink).toBe("/my-post");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("generates permalink from multi-level path", () => {
|
|
117
|
+
const post = makePostWithMedia({ id: 123, path: "2024/01/my-post" });
|
|
118
|
+
const view = toPostView(post, EMPTY_CTX);
|
|
119
|
+
expect(view.permalink).toBe("/2024/01/my-post");
|
|
120
|
+
});
|
|
121
|
+
|
|
105
122
|
it("formats dates correctly", () => {
|
|
106
123
|
const post = makePostWithMedia({ publishedAt: 1706745600 });
|
|
107
124
|
const view = toPostView(post, EMPTY_CTX);
|
|
@@ -109,52 +126,156 @@ describe("toPostView", () => {
|
|
|
109
126
|
expect(view.publishedAtFormatted).toBe("Feb 1, 2024");
|
|
110
127
|
});
|
|
111
128
|
|
|
112
|
-
it("generates excerpt from
|
|
113
|
-
const
|
|
114
|
-
const
|
|
129
|
+
it("generates excerpt from body", () => {
|
|
130
|
+
const shortBody = "Short text";
|
|
131
|
+
const longBody = "A".repeat(200);
|
|
115
132
|
|
|
116
133
|
const shortView = toPostView(
|
|
117
|
-
makePostWithMedia({
|
|
134
|
+
makePostWithMedia({ body: shortBody }),
|
|
118
135
|
EMPTY_CTX,
|
|
119
136
|
);
|
|
120
137
|
expect(shortView.excerpt).toBe("Short text");
|
|
121
138
|
|
|
122
139
|
const longView = toPostView(
|
|
123
|
-
makePostWithMedia({
|
|
140
|
+
makePostWithMedia({ body: longBody }),
|
|
124
141
|
EMPTY_CTX,
|
|
125
142
|
);
|
|
126
143
|
expect(longView.excerpt).toBe("A".repeat(160) + "...");
|
|
127
144
|
});
|
|
128
145
|
|
|
129
|
-
it("
|
|
130
|
-
const view = toPostView(
|
|
146
|
+
it("computes summaryHtml for posts with title and bodyHtml", () => {
|
|
147
|
+
const view = toPostView(
|
|
148
|
+
makePostWithMedia({
|
|
149
|
+
title: "My Article",
|
|
150
|
+
body: "Short article body",
|
|
151
|
+
bodyHtml: "<p>Short article body</p>",
|
|
152
|
+
}),
|
|
153
|
+
EMPTY_CTX,
|
|
154
|
+
);
|
|
155
|
+
expect(view.summaryHtml).toBe("<p>Short article body</p>");
|
|
156
|
+
expect(view.summaryHasMore).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("truncates summaryHtml for long articles", () => {
|
|
160
|
+
const p1 = `<p>${"A".repeat(300)}</p>`;
|
|
161
|
+
const p2 = `<p>${"B".repeat(300)}</p>`;
|
|
162
|
+
const view = toPostView(
|
|
163
|
+
makePostWithMedia({
|
|
164
|
+
title: "Long Article",
|
|
165
|
+
body: "A".repeat(300) + "B".repeat(300),
|
|
166
|
+
bodyHtml: p1 + p2,
|
|
167
|
+
}),
|
|
168
|
+
EMPTY_CTX,
|
|
169
|
+
);
|
|
170
|
+
expect(view.summaryHtml).toBe(p1);
|
|
171
|
+
expect(view.summaryHasMore).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("does not compute summaryHtml for posts without title", () => {
|
|
175
|
+
const view = toPostView(
|
|
176
|
+
makePostWithMedia({
|
|
177
|
+
title: null,
|
|
178
|
+
bodyHtml: "<p>Just a note</p>",
|
|
179
|
+
}),
|
|
180
|
+
EMPTY_CTX,
|
|
181
|
+
);
|
|
182
|
+
expect(view.summaryHtml).toBeUndefined();
|
|
183
|
+
expect(view.summaryHasMore).toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("does not compute summaryHtml for posts without bodyHtml", () => {
|
|
187
|
+
const view = toPostView(
|
|
188
|
+
makePostWithMedia({
|
|
189
|
+
title: "Title Only",
|
|
190
|
+
bodyHtml: null,
|
|
191
|
+
}),
|
|
192
|
+
EMPTY_CTX,
|
|
193
|
+
);
|
|
194
|
+
expect(view.summaryHtml).toBeUndefined();
|
|
195
|
+
expect(view.summaryHasMore).toBeUndefined();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("handles null body gracefully", () => {
|
|
199
|
+
const view = toPostView(
|
|
200
|
+
makePostWithMedia({ body: null, bodyHtml: null }),
|
|
201
|
+
EMPTY_CTX,
|
|
202
|
+
);
|
|
131
203
|
expect(view.excerpt).toBeUndefined();
|
|
132
|
-
expect(view.
|
|
204
|
+
expect(view.bodyHtml).toBeUndefined();
|
|
205
|
+
expect(view.body).toBeUndefined();
|
|
133
206
|
});
|
|
134
207
|
|
|
135
208
|
it("converts null fields to undefined", () => {
|
|
136
209
|
const view = toPostView(makePostWithMedia(), EMPTY_CTX);
|
|
137
210
|
expect(view.title).toBeUndefined();
|
|
138
211
|
expect(view.path).toBeUndefined();
|
|
139
|
-
expect(view.
|
|
140
|
-
expect(view.
|
|
141
|
-
expect(view.
|
|
212
|
+
expect(view.url).toBeUndefined();
|
|
213
|
+
expect(view.quoteText).toBeUndefined();
|
|
214
|
+
expect(view.rating).toBeUndefined();
|
|
215
|
+
expect(view.collectionId).toBeUndefined();
|
|
142
216
|
expect(view.replyToId).toBeUndefined();
|
|
143
217
|
expect(view.threadRootId).toBeUndefined();
|
|
144
218
|
});
|
|
145
219
|
|
|
146
|
-
it("preserves non-null
|
|
220
|
+
it("preserves non-null url field", () => {
|
|
147
221
|
const view = toPostView(
|
|
148
222
|
makePostWithMedia({
|
|
149
|
-
|
|
150
|
-
sourceName: "Example",
|
|
151
|
-
sourceDomain: "example.com",
|
|
223
|
+
url: "https://example.com",
|
|
152
224
|
}),
|
|
153
225
|
EMPTY_CTX,
|
|
154
226
|
);
|
|
155
|
-
expect(view.
|
|
156
|
-
|
|
157
|
-
|
|
227
|
+
expect(view.url).toBe("https://example.com");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("preserves non-null quoteText field", () => {
|
|
231
|
+
const view = toPostView(
|
|
232
|
+
makePostWithMedia({
|
|
233
|
+
format: "quote",
|
|
234
|
+
quoteText: "Something wise",
|
|
235
|
+
}),
|
|
236
|
+
EMPTY_CTX,
|
|
237
|
+
);
|
|
238
|
+
expect(view.quoteText).toBe("Something wise");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("maps format, status, featured, and pinned correctly", () => {
|
|
242
|
+
const view = toPostView(
|
|
243
|
+
makePostWithMedia({
|
|
244
|
+
format: "link",
|
|
245
|
+
status: "draft",
|
|
246
|
+
featured: 1,
|
|
247
|
+
pinned: 1,
|
|
248
|
+
}),
|
|
249
|
+
EMPTY_CTX,
|
|
250
|
+
);
|
|
251
|
+
expect(view.format).toBe("link");
|
|
252
|
+
expect(view.status).toBe("draft");
|
|
253
|
+
expect(view.featured).toBe(true);
|
|
254
|
+
expect(view.pinned).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("converts featured=0 and pinned=0 to false", () => {
|
|
258
|
+
const view = toPostView(
|
|
259
|
+
makePostWithMedia({
|
|
260
|
+
featured: 0,
|
|
261
|
+
pinned: 0,
|
|
262
|
+
}),
|
|
263
|
+
EMPTY_CTX,
|
|
264
|
+
);
|
|
265
|
+
expect(view.featured).toBe(false);
|
|
266
|
+
expect(view.pinned).toBe(false);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("preserves rating and collectionId when set", () => {
|
|
270
|
+
const view = toPostView(
|
|
271
|
+
makePostWithMedia({
|
|
272
|
+
rating: 5,
|
|
273
|
+
collectionId: 42,
|
|
274
|
+
}),
|
|
275
|
+
EMPTY_CTX,
|
|
276
|
+
);
|
|
277
|
+
expect(view.rating).toBe(5);
|
|
278
|
+
expect(view.collectionId).toBe(42);
|
|
158
279
|
});
|
|
159
280
|
|
|
160
281
|
it("converts media attachments to MediaView", () => {
|
|
@@ -249,40 +370,40 @@ describe("toMediaView", () => {
|
|
|
249
370
|
});
|
|
250
371
|
|
|
251
372
|
// =============================================================================
|
|
252
|
-
//
|
|
373
|
+
// toNavItemView
|
|
253
374
|
// =============================================================================
|
|
254
375
|
|
|
255
|
-
describe("
|
|
376
|
+
describe("toNavItemView", () => {
|
|
256
377
|
it("marks home link active on exact / match", () => {
|
|
257
|
-
const view =
|
|
378
|
+
const view = toNavItemView(makeNavItem({ url: "/" }), "/");
|
|
258
379
|
expect(view.isActive).toBe(true);
|
|
259
380
|
expect(view.isExternal).toBe(false);
|
|
260
381
|
});
|
|
261
382
|
|
|
262
383
|
it("marks home link inactive on other paths", () => {
|
|
263
|
-
const view =
|
|
384
|
+
const view = toNavItemView(makeNavItem({ url: "/" }), "/archive");
|
|
264
385
|
expect(view.isActive).toBe(false);
|
|
265
386
|
});
|
|
266
387
|
|
|
267
388
|
it("matches prefix for non-root links", () => {
|
|
268
|
-
const view =
|
|
389
|
+
const view = toNavItemView(makeNavItem({ url: "/archive" }), "/archive");
|
|
269
390
|
expect(view.isActive).toBe(true);
|
|
270
391
|
|
|
271
|
-
const viewSub =
|
|
272
|
-
|
|
392
|
+
const viewSub = toNavItemView(
|
|
393
|
+
makeNavItem({ url: "/archive" }),
|
|
273
394
|
"/archive/2024",
|
|
274
395
|
);
|
|
275
396
|
expect(viewSub.isActive).toBe(true);
|
|
276
397
|
});
|
|
277
398
|
|
|
278
399
|
it("does not false-match similar prefixes", () => {
|
|
279
|
-
const view =
|
|
400
|
+
const view = toNavItemView(makeNavItem({ url: "/arch" }), "/archive");
|
|
280
401
|
expect(view.isActive).toBe(false);
|
|
281
402
|
});
|
|
282
403
|
|
|
283
404
|
it("marks external links as external and never active", () => {
|
|
284
|
-
const view =
|
|
285
|
-
|
|
405
|
+
const view = toNavItemView(
|
|
406
|
+
makeNavItem({ url: "https://example.com" }),
|
|
286
407
|
"/",
|
|
287
408
|
);
|
|
288
409
|
expect(view.isExternal).toBe(true);
|
|
@@ -290,20 +411,31 @@ describe("toNavLinkView", () => {
|
|
|
290
411
|
});
|
|
291
412
|
|
|
292
413
|
it("handles http:// links", () => {
|
|
293
|
-
const view =
|
|
414
|
+
const view = toNavItemView(makeNavItem({ url: "http://example.com" }), "/");
|
|
294
415
|
expect(view.isExternal).toBe(true);
|
|
295
416
|
expect(view.isActive).toBe(false);
|
|
296
417
|
});
|
|
418
|
+
|
|
419
|
+
it("includes type and pageId in view", () => {
|
|
420
|
+
const view = toNavItemView(makeNavItem({ type: "page", pageId: 5 }), "/");
|
|
421
|
+
expect(view.type).toBe("page");
|
|
422
|
+
expect(view.pageId).toBe(5);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("converts null pageId to undefined", () => {
|
|
426
|
+
const view = toNavItemView(makeNavItem({ pageId: null }), "/");
|
|
427
|
+
expect(view.pageId).toBeUndefined();
|
|
428
|
+
});
|
|
297
429
|
});
|
|
298
430
|
|
|
299
|
-
describe("
|
|
300
|
-
it("converts multiple
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
431
|
+
describe("toNavItemViews", () => {
|
|
432
|
+
it("converts multiple items", () => {
|
|
433
|
+
const items = [
|
|
434
|
+
makeNavItem({ id: 1, url: "/" }),
|
|
435
|
+
makeNavItem({ id: 2, url: "/archive" }),
|
|
436
|
+
makeNavItem({ id: 3, url: "https://github.com" }),
|
|
305
437
|
];
|
|
306
|
-
const views =
|
|
438
|
+
const views = toNavItemViews(items, "/archive");
|
|
307
439
|
expect(views).toHaveLength(3);
|
|
308
440
|
expect(views[0]).toHaveProperty("isActive", false);
|
|
309
441
|
expect(views[1]).toHaveProperty("isActive", true);
|
|
@@ -329,6 +461,28 @@ describe("toSearchResultView", () => {
|
|
|
329
461
|
expect(view.rank).toBe(1.5);
|
|
330
462
|
expect(view.snippet).toBe("...matching <b>text</b>...");
|
|
331
463
|
});
|
|
464
|
+
|
|
465
|
+
it("uses new post fields in search result view", () => {
|
|
466
|
+
const result: SearchResult = {
|
|
467
|
+
post: makePost({
|
|
468
|
+
id: 10,
|
|
469
|
+
format: "link",
|
|
470
|
+
status: "published",
|
|
471
|
+
featured: 1,
|
|
472
|
+
pinned: 0,
|
|
473
|
+
url: "https://example.com",
|
|
474
|
+
path: "my-link",
|
|
475
|
+
}),
|
|
476
|
+
rank: 0.8,
|
|
477
|
+
};
|
|
478
|
+
const view = toSearchResultView(result, EMPTY_CTX);
|
|
479
|
+
expect(view.post.format).toBe("link");
|
|
480
|
+
expect(view.post.status).toBe("published");
|
|
481
|
+
expect(view.post.featured).toBe(true);
|
|
482
|
+
expect(view.post.pinned).toBe(false);
|
|
483
|
+
expect(view.post.url).toBe("https://example.com");
|
|
484
|
+
expect(view.post.permalink).toBe("/my-link");
|
|
485
|
+
});
|
|
332
486
|
});
|
|
333
487
|
|
|
334
488
|
// =============================================================================
|
package/src/lib/constants.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export const RESERVED_PATHS = [
|
|
9
9
|
"featured",
|
|
10
|
+
"collections",
|
|
10
11
|
"signin",
|
|
11
12
|
"signout",
|
|
12
13
|
"setup",
|
|
@@ -15,10 +16,6 @@ export const RESERVED_PATHS = [
|
|
|
15
16
|
"feed",
|
|
16
17
|
"search",
|
|
17
18
|
"archive",
|
|
18
|
-
"notes",
|
|
19
|
-
"articles",
|
|
20
|
-
"links",
|
|
21
|
-
"quotes",
|
|
22
19
|
"media",
|
|
23
20
|
"pages",
|
|
24
21
|
"reset",
|
|
@@ -53,6 +50,7 @@ export const SETTINGS_KEYS = {
|
|
|
53
50
|
SITE_DESCRIPTION: "SITE_DESCRIPTION",
|
|
54
51
|
SITE_LANGUAGE: "SITE_LANGUAGE",
|
|
55
52
|
THEME: "THEME",
|
|
53
|
+
CUSTOM_CSS: "CUSTOM_CSS",
|
|
56
54
|
PASSWORD_RESET_TOKEN: "PASSWORD_RESET_TOKEN",
|
|
57
55
|
} as const;
|
|
58
56
|
|