@jant/core 0.3.22 → 0.3.24
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 +23 -5
- 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 -6
- package/dist/lib/constants.js +1 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/navigation.js +4 -5
- package/dist/lib/render.js +1 -1
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme-components.js +8 -11
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +119 -0
- package/dist/lib/view.js +62 -73
- package/dist/routes/api/posts.js +29 -35
- package/dist/routes/api/search.js +5 -6
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/dash/collections.js +22 -40
- package/dist/routes/dash/index.js +2 -2
- package/dist/routes/dash/navigation.js +25 -24
- package/dist/routes/dash/pages.js +42 -57
- package/dist/routes/dash/posts.js +27 -35
- package/dist/routes/feed/rss.js +2 -4
- package/dist/routes/feed/sitemap.js +10 -7
- package/dist/routes/pages/archive.js +12 -11
- package/dist/routes/pages/collection.js +11 -5
- package/dist/routes/pages/home.js +53 -61
- package/dist/routes/pages/page.js +60 -29
- package/dist/routes/pages/post.js +5 -12
- package/dist/routes/pages/search.js +3 -4
- 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 +80 -0
- package/dist/services/post.js +68 -69
- package/dist/services/search.js +24 -18
- package/dist/theme/components/MediaGallery.js +19 -91
- package/dist/theme/components/PageForm.js +15 -15
- package/dist/theme/components/PostForm.js +136 -129
- package/dist/theme/components/PostList.js +13 -8
- package/dist/theme/components/ThreadView.js +3 -3
- package/dist/theme/components/TypeBadge.js +3 -14
- package/dist/theme/components/VisibilityBadge.js +33 -23
- package/dist/theme/components/index.js +0 -2
- package/dist/theme/index.js +10 -16
- package/dist/theme/layouts/index.js +0 -1
- package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
- package/dist/themes/threads/index.js +81 -0
- package/dist/{theme → themes/threads}/pages/ArchivePage.js +31 -47
- package/dist/themes/threads/pages/CollectionPage.js +65 -0
- package/dist/{theme → themes/threads}/pages/HomePage.js +4 -5
- package/dist/{theme → themes/threads}/pages/PostPage.js +10 -8
- package/dist/{theme → themes/threads}/pages/SearchPage.js +8 -8
- package/dist/{theme → themes/threads}/pages/SinglePage.js +5 -6
- package/dist/{theme/components → themes/threads}/timeline/LinkCard.js +20 -11
- package/dist/themes/threads/timeline/NoteCard.js +53 -0
- package/dist/themes/threads/timeline/QuoteCard.js +59 -0
- package/dist/{theme/components → themes/threads}/timeline/ThreadPreview.js +5 -6
- package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
- package/dist/{theme/components → themes/threads}/timeline/TimelineItem.js +8 -17
- package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
- package/dist/themes/threads/timeline/groupByDate.js +22 -0
- package/dist/themes/threads/timeline/timelineMore.js +107 -0
- package/dist/types.js +24 -40
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +51 -74
- package/src/app.tsx +27 -6
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +216 -164
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +216 -164
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +216 -164
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +30 -15
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +166 -105
- package/src/lib/__tests__/theme-components.test.ts +4 -25
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
- package/src/lib/__tests__/view.test.ts +217 -67
- package/src/lib/constants.ts +1 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/navigation.ts +6 -7
- package/src/lib/render.tsx +1 -1
- package/src/lib/schemas.ts +118 -52
- package/src/lib/theme-components.ts +10 -13
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +170 -0
- package/src/lib/view.ts +81 -83
- package/src/preset.css +45 -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/posts.ts +30 -30
- package/src/routes/api/search.ts +4 -4
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/dash/collections.tsx +18 -40
- package/src/routes/dash/index.tsx +2 -2
- package/src/routes/dash/navigation.tsx +27 -26
- package/src/routes/dash/pages.tsx +45 -60
- package/src/routes/dash/posts.tsx +44 -52
- package/src/routes/feed/rss.ts +2 -1
- package/src/routes/feed/sitemap.ts +14 -4
- package/src/routes/pages/archive.tsx +14 -10
- package/src/routes/pages/collection.tsx +17 -6
- package/src/routes/pages/home.tsx +56 -81
- package/src/routes/pages/page.tsx +64 -27
- package/src/routes/pages/post.tsx +5 -14
- package/src/routes/pages/search.tsx +2 -2
- 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__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +342 -206
- 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 +124 -0
- package/src/services/post.ts +93 -103
- package/src/services/search.ts +38 -27
- package/src/styles/components.css +0 -54
- package/src/theme/components/MediaGallery.tsx +27 -96
- package/src/theme/components/PageForm.tsx +21 -21
- package/src/theme/components/PostForm.tsx +122 -118
- package/src/theme/components/PostList.tsx +58 -49
- package/src/theme/components/ThreadView.tsx +6 -3
- package/src/theme/components/TypeBadge.tsx +9 -17
- package/src/theme/components/VisibilityBadge.tsx +40 -23
- package/src/theme/components/index.ts +0 -13
- package/src/theme/index.ts +10 -16
- package/src/theme/layouts/index.ts +0 -1
- package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
- package/src/themes/threads/index.ts +100 -0
- package/src/{theme → themes/threads}/pages/ArchivePage.tsx +52 -55
- package/src/themes/threads/pages/CollectionPage.tsx +61 -0
- package/src/{theme → themes/threads}/pages/HomePage.tsx +5 -6
- package/src/{theme → themes/threads}/pages/PostPage.tsx +11 -8
- package/src/{theme → themes/threads}/pages/SearchPage.tsx +9 -13
- package/src/themes/threads/pages/SinglePage.tsx +23 -0
- package/src/themes/threads/style.css +336 -0
- package/src/{theme/components → themes/threads}/timeline/LinkCard.tsx +21 -13
- package/src/themes/threads/timeline/NoteCard.tsx +58 -0
- package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
- package/src/{theme/components → themes/threads}/timeline/ThreadPreview.tsx +6 -6
- package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
- package/src/{theme/components → themes/threads}/timeline/TimelineItem.tsx +9 -20
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
- package/src/themes/threads/timeline/groupByDate.ts +30 -0
- package/src/themes/threads/timeline/timelineMore.tsx +130 -0
- package/src/types.ts +242 -98
- package/dist/routes/api/timeline.js +0 -120
- package/dist/theme/components/timeline/ArticleCard.js +0 -46
- package/dist/theme/components/timeline/ImageCard.js +0 -83
- package/dist/theme/components/timeline/NoteCard.js +0 -34
- package/dist/theme/components/timeline/QuoteCard.js +0 -48
- package/dist/theme/components/timeline/TimelineFeed.js +0 -46
- package/dist/theme/components/timeline/index.js +0 -8
- package/dist/theme/layouts/SiteLayout.js +0 -131
- package/dist/theme/pages/CollectionPage.js +0 -63
- package/dist/theme/pages/index.js +0 -11
- package/src/routes/api/timeline.tsx +0 -159
- package/src/theme/components/timeline/ArticleCard.tsx +0 -45
- package/src/theme/components/timeline/ImageCard.tsx +0 -70
- package/src/theme/components/timeline/NoteCard.tsx +0 -34
- package/src/theme/components/timeline/QuoteCard.tsx +0 -48
- package/src/theme/components/timeline/TimelineFeed.tsx +0 -56
- package/src/theme/components/timeline/index.ts +0 -8
- package/src/theme/layouts/SiteLayout.tsx +0 -132
- package/src/theme/pages/CollectionPage.tsx +0 -60
- package/src/theme/pages/SinglePage.tsx +0 -24
- package/src/theme/pages/index.ts +0 -13
|
@@ -5,28 +5,23 @@ import type {
|
|
|
5
5
|
TimelineCardProps,
|
|
6
6
|
ThreadPreviewProps,
|
|
7
7
|
TimelineFeedProps,
|
|
8
|
-
|
|
8
|
+
Format,
|
|
9
9
|
HomePageProps,
|
|
10
10
|
} from "../../types.js";
|
|
11
11
|
import type { FC } from "hono/jsx";
|
|
12
12
|
|
|
13
13
|
// Create simple mock components for testing (avoids importing .tsx files with i18n)
|
|
14
14
|
const MockNoteCard: FC<TimelineCardProps> = () => null;
|
|
15
|
-
const MockArticleCard: FC<TimelineCardProps> = () => null;
|
|
16
15
|
const MockLinkCard: FC<TimelineCardProps> = () => null;
|
|
17
16
|
const MockQuoteCard: FC<TimelineCardProps> = () => null;
|
|
18
|
-
const MockImageCard: FC<TimelineCardProps> = () => null;
|
|
19
17
|
const MockThreadPreview: FC<ThreadPreviewProps> = () => null;
|
|
20
18
|
const MockTimelineFeed: FC<TimelineFeedProps> = () => null;
|
|
21
19
|
const MockHomePage: FC<HomePageProps> = () => null;
|
|
22
20
|
|
|
23
|
-
const DEFAULT_CARD_MAP: Record<
|
|
21
|
+
const DEFAULT_CARD_MAP: Record<Format, FC<TimelineCardProps>> = {
|
|
24
22
|
note: MockNoteCard,
|
|
25
|
-
article: MockArticleCard,
|
|
26
23
|
link: MockLinkCard,
|
|
27
24
|
quote: MockQuoteCard,
|
|
28
|
-
image: MockImageCard,
|
|
29
|
-
page: MockNoteCard,
|
|
30
25
|
};
|
|
31
26
|
|
|
32
27
|
describe("theme-components", () => {
|
|
@@ -35,12 +30,6 @@ describe("theme-components", () => {
|
|
|
35
30
|
expect(resolveCardComponent("note", DEFAULT_CARD_MAP)).toBe(MockNoteCard);
|
|
36
31
|
});
|
|
37
32
|
|
|
38
|
-
it("returns default ArticleCard for article type", () => {
|
|
39
|
-
expect(resolveCardComponent("article", DEFAULT_CARD_MAP)).toBe(
|
|
40
|
-
MockArticleCard,
|
|
41
|
-
);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
33
|
it("returns default LinkCard for link type", () => {
|
|
45
34
|
expect(resolveCardComponent("link", DEFAULT_CARD_MAP)).toBe(MockLinkCard);
|
|
46
35
|
});
|
|
@@ -51,16 +40,6 @@ describe("theme-components", () => {
|
|
|
51
40
|
);
|
|
52
41
|
});
|
|
53
42
|
|
|
54
|
-
it("returns default ImageCard for image type", () => {
|
|
55
|
-
expect(resolveCardComponent("image", DEFAULT_CARD_MAP)).toBe(
|
|
56
|
-
MockImageCard,
|
|
57
|
-
);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("returns NoteCard as fallback for page type", () => {
|
|
61
|
-
expect(resolveCardComponent("page", DEFAULT_CARD_MAP)).toBe(MockNoteCard);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
43
|
it("returns theme override when provided", () => {
|
|
65
44
|
const CustomNote: FC<TimelineCardProps> = () => null;
|
|
66
45
|
const overrides: ThemeComponents = { NoteCard: CustomNote };
|
|
@@ -71,8 +50,8 @@ describe("theme-components", () => {
|
|
|
71
50
|
|
|
72
51
|
it("returns default when theme has no override for type", () => {
|
|
73
52
|
const overrides: ThemeComponents = {};
|
|
74
|
-
expect(resolveCardComponent("
|
|
75
|
-
|
|
53
|
+
expect(resolveCardComponent("link", DEFAULT_CARD_MAP, overrides)).toBe(
|
|
54
|
+
MockLinkCard,
|
|
76
55
|
);
|
|
77
56
|
});
|
|
78
57
|
});
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
isWithinMonth,
|
|
5
5
|
toISOString,
|
|
6
6
|
formatDate,
|
|
7
|
+
formatTime,
|
|
8
|
+
formatRelativeTime,
|
|
7
9
|
formatYearMonth,
|
|
8
10
|
} from "../time.js";
|
|
9
11
|
|
|
@@ -91,6 +93,66 @@ describe("formatDate", () => {
|
|
|
91
93
|
});
|
|
92
94
|
});
|
|
93
95
|
|
|
96
|
+
describe("formatTime", () => {
|
|
97
|
+
it("formats as HH:MM in 24-hour format", () => {
|
|
98
|
+
// 2024-01-15T12:30:00Z = 1705321800
|
|
99
|
+
expect(formatTime(1705321800)).toBe("12:30");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("zero-pads hours and minutes", () => {
|
|
103
|
+
// 2024-02-01T00:00:00Z = 1706745600
|
|
104
|
+
expect(formatTime(1706745600)).toBe("00:00");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("formats evening time correctly", () => {
|
|
108
|
+
// 2024-02-01T23:05:00Z = 1706828700
|
|
109
|
+
expect(formatTime(1706828700)).toBe("23:05");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("formats single-digit hour with padding", () => {
|
|
113
|
+
// 2024-02-01T09:07:00Z = 1706778420
|
|
114
|
+
expect(formatTime(1706778420)).toBe("09:07");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("formatRelativeTime", () => {
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
vi.restoreAllMocks();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("returns '1m' for timestamps less than 60 seconds ago", () => {
|
|
124
|
+
expect(formatRelativeTime(now() - 10)).toBe("1m");
|
|
125
|
+
expect(formatRelativeTime(now() - 59)).toBe("1m");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("returns minutes for timestamps under 1 hour", () => {
|
|
129
|
+
expect(formatRelativeTime(now() - 60)).toBe("1m");
|
|
130
|
+
expect(formatRelativeTime(now() - 300)).toBe("5m");
|
|
131
|
+
expect(formatRelativeTime(now() - 3540)).toBe("59m");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("returns hours for timestamps under 24 hours", () => {
|
|
135
|
+
expect(formatRelativeTime(now() - 3600)).toBe("1h");
|
|
136
|
+
expect(formatRelativeTime(now() - 7200)).toBe("2h");
|
|
137
|
+
expect(formatRelativeTime(now() - 82800)).toBe("23h");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("returns days for timestamps up to 7 days", () => {
|
|
141
|
+
expect(formatRelativeTime(now() - 86400)).toBe("1d");
|
|
142
|
+
expect(formatRelativeTime(now() - 3 * 86400)).toBe("3d");
|
|
143
|
+
expect(formatRelativeTime(now() - 7 * 86400)).toBe("7d");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("returns 'MMM D' for timestamps older than 7 days", () => {
|
|
147
|
+
// Use a fixed timestamp to get a predictable date
|
|
148
|
+
// Feb 1, 2024 00:00:00 UTC
|
|
149
|
+
const feb1 = 1706745600;
|
|
150
|
+
// Mock now() to return Feb 16, 2024
|
|
151
|
+
vi.spyOn(Date, "now").mockReturnValue((feb1 + 15 * 86400) * 1000);
|
|
152
|
+
expect(formatRelativeTime(feb1)).toBe("Feb 1");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
94
156
|
describe("formatYearMonth", () => {
|
|
95
157
|
it("formats as YYYY-MM", () => {
|
|
96
158
|
expect(formatYearMonth(1706745600)).toBe("2024-02");
|
|
@@ -1,19 +1,20 @@
|
|
|
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
|
|
16
|
-
import type {
|
|
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 { groupByDate } from "../timeline.js";
|
|
16
|
+
import type { Database } from "../../db/index.js";
|
|
17
|
+
import type { PostWithMedia, TimelineItemView } from "../../types.js";
|
|
17
18
|
|
|
18
19
|
describe("Timeline data assembly", () => {
|
|
19
20
|
let db: Database;
|
|
@@ -29,15 +30,13 @@ describe("Timeline data assembly", () => {
|
|
|
29
30
|
|
|
30
31
|
it("assembles timeline items with media attachments", async () => {
|
|
31
32
|
const post = await postService.create({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
visibility: "featured",
|
|
33
|
+
format: "note",
|
|
34
|
+
body: "Hello",
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
const posts = await postService.list({
|
|
38
|
-
|
|
38
|
+
status: "published",
|
|
39
39
|
excludeReplies: true,
|
|
40
|
-
excludeTypes: ["page"],
|
|
41
40
|
limit: 21,
|
|
42
41
|
});
|
|
43
42
|
|
|
@@ -60,25 +59,23 @@ describe("Timeline data assembly", () => {
|
|
|
60
59
|
|
|
61
60
|
it("identifies thread roots and builds thread previews", async () => {
|
|
62
61
|
const root = await postService.create({
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
visibility: "featured",
|
|
62
|
+
format: "note",
|
|
63
|
+
body: "Thread root",
|
|
66
64
|
});
|
|
67
65
|
await postService.create({
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
format: "note",
|
|
67
|
+
body: "Reply 1",
|
|
70
68
|
replyToId: root.id,
|
|
71
69
|
});
|
|
72
70
|
await postService.create({
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
format: "note",
|
|
72
|
+
body: "Reply 2",
|
|
75
73
|
replyToId: root.id,
|
|
76
74
|
});
|
|
77
75
|
|
|
78
76
|
const posts = await postService.list({
|
|
79
|
-
|
|
77
|
+
status: "published",
|
|
80
78
|
excludeReplies: true,
|
|
81
|
-
excludeTypes: ["page"],
|
|
82
79
|
limit: 21,
|
|
83
80
|
});
|
|
84
81
|
|
|
@@ -96,7 +93,7 @@ describe("Timeline data assembly", () => {
|
|
|
96
93
|
const threadPreviews = await postService.getThreadPreviews(threadRootIds);
|
|
97
94
|
const replies = threadPreviews.get(root.id);
|
|
98
95
|
expect(replies).toHaveLength(2);
|
|
99
|
-
expect(replies?.[0]?.
|
|
96
|
+
expect(replies?.[0]?.body).toBe("Reply 1");
|
|
100
97
|
|
|
101
98
|
// Assemble items
|
|
102
99
|
const rawMediaMap = await mediaService.getByPostIds(postIds);
|
|
@@ -133,50 +130,25 @@ describe("Timeline data assembly", () => {
|
|
|
133
130
|
expect(items[0]?.threadPreview?.totalReplyCount).toBe(2);
|
|
134
131
|
});
|
|
135
132
|
|
|
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
133
|
it("excludes replies from top-level list", async () => {
|
|
160
134
|
const root = await postService.create({
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
visibility: "quiet",
|
|
135
|
+
format: "note",
|
|
136
|
+
body: "Root",
|
|
164
137
|
});
|
|
165
138
|
await postService.create({
|
|
166
|
-
|
|
167
|
-
|
|
139
|
+
format: "note",
|
|
140
|
+
body: "Reply",
|
|
168
141
|
replyToId: root.id,
|
|
169
142
|
});
|
|
170
143
|
|
|
171
144
|
const posts = await postService.list({
|
|
172
|
-
|
|
145
|
+
status: "published",
|
|
173
146
|
excludeReplies: true,
|
|
174
|
-
excludeTypes: ["page"],
|
|
175
147
|
limit: 21,
|
|
176
148
|
});
|
|
177
149
|
|
|
178
150
|
expect(posts).toHaveLength(1);
|
|
179
|
-
expect(posts[0]?.
|
|
151
|
+
expect(posts[0]?.body).toBe("Root");
|
|
180
152
|
});
|
|
181
153
|
|
|
182
154
|
it("supports cursor pagination for load more", async () => {
|
|
@@ -184,9 +156,8 @@ describe("Timeline data assembly", () => {
|
|
|
184
156
|
for (let i = 0; i < 5; i++) {
|
|
185
157
|
posts.push(
|
|
186
158
|
await postService.create({
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
visibility: "quiet",
|
|
159
|
+
format: "note",
|
|
160
|
+
body: `Post ${i}`,
|
|
190
161
|
publishedAt: 1000 + i,
|
|
191
162
|
}),
|
|
192
163
|
);
|
|
@@ -194,9 +165,8 @@ describe("Timeline data assembly", () => {
|
|
|
194
165
|
|
|
195
166
|
// First page
|
|
196
167
|
const page1 = await postService.list({
|
|
197
|
-
|
|
168
|
+
status: "published",
|
|
198
169
|
excludeReplies: true,
|
|
199
|
-
excludeTypes: ["page"],
|
|
200
170
|
limit: 3,
|
|
201
171
|
});
|
|
202
172
|
expect(page1).toHaveLength(3);
|
|
@@ -205,9 +175,8 @@ describe("Timeline data assembly", () => {
|
|
|
205
175
|
const lastPost = page1[page1.length - 1];
|
|
206
176
|
expect(lastPost).toBeDefined();
|
|
207
177
|
const page2 = await postService.list({
|
|
208
|
-
|
|
178
|
+
status: "published",
|
|
209
179
|
excludeReplies: true,
|
|
210
|
-
excludeTypes: ["page"],
|
|
211
180
|
limit: 3,
|
|
212
181
|
cursor: lastPost?.id,
|
|
213
182
|
});
|
|
@@ -218,18 +187,16 @@ describe("Timeline data assembly", () => {
|
|
|
218
187
|
it("correctly determines hasMore flag", async () => {
|
|
219
188
|
for (let i = 0; i < 3; i++) {
|
|
220
189
|
await postService.create({
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
visibility: "quiet",
|
|
190
|
+
format: "note",
|
|
191
|
+
body: `Post ${i}`,
|
|
224
192
|
});
|
|
225
193
|
}
|
|
226
194
|
|
|
227
195
|
// Request limit + 1 to check for more
|
|
228
196
|
const pageSize = 2;
|
|
229
197
|
const posts = await postService.list({
|
|
230
|
-
|
|
198
|
+
status: "published",
|
|
231
199
|
excludeReplies: true,
|
|
232
|
-
excludeTypes: ["page"],
|
|
233
200
|
limit: pageSize + 1,
|
|
234
201
|
});
|
|
235
202
|
|
|
@@ -240,3 +207,78 @@ describe("Timeline data assembly", () => {
|
|
|
240
207
|
expect(displayPosts).toHaveLength(2);
|
|
241
208
|
});
|
|
242
209
|
});
|
|
210
|
+
|
|
211
|
+
describe("groupByDate", () => {
|
|
212
|
+
function makeItem(dateStr: string, formatted: string): TimelineItemView {
|
|
213
|
+
return {
|
|
214
|
+
post: {
|
|
215
|
+
id: 1,
|
|
216
|
+
permalink: "/p/1",
|
|
217
|
+
format: "note",
|
|
218
|
+
status: "published",
|
|
219
|
+
featured: true,
|
|
220
|
+
pinned: false,
|
|
221
|
+
publishedAt: `${dateStr}T12:00:00.000Z`,
|
|
222
|
+
publishedAtFormatted: formatted,
|
|
223
|
+
publishedAtTime: "12:00",
|
|
224
|
+
publishedAtRelative: "1d",
|
|
225
|
+
updatedAt: `${dateStr}T12:00:00.000Z`,
|
|
226
|
+
media: [],
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
it("returns empty array for empty input", () => {
|
|
232
|
+
expect(groupByDate([])).toEqual([]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("groups items by YYYY-MM-DD date key", () => {
|
|
236
|
+
const items = [
|
|
237
|
+
makeItem("2024-02-01", "Feb 1, 2024"),
|
|
238
|
+
makeItem("2024-02-01", "Feb 1, 2024"),
|
|
239
|
+
makeItem("2024-02-02", "Feb 2, 2024"),
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
const groups = groupByDate(items);
|
|
243
|
+
expect(groups).toHaveLength(2);
|
|
244
|
+
expect(groups[0]?.dateKey).toBe("2024-02-01");
|
|
245
|
+
expect(groups[0]?.label).toBe("Feb 1, 2024");
|
|
246
|
+
expect(groups[0]?.items).toHaveLength(2);
|
|
247
|
+
expect(groups[1]?.dateKey).toBe("2024-02-02");
|
|
248
|
+
expect(groups[1]?.items).toHaveLength(1);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("creates separate groups for non-contiguous same dates", () => {
|
|
252
|
+
const items = [
|
|
253
|
+
makeItem("2024-02-01", "Feb 1, 2024"),
|
|
254
|
+
makeItem("2024-02-02", "Feb 2, 2024"),
|
|
255
|
+
makeItem("2024-02-01", "Feb 1, 2024"),
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
const groups = groupByDate(items);
|
|
259
|
+
expect(groups).toHaveLength(3);
|
|
260
|
+
expect(groups[0]?.dateKey).toBe("2024-02-01");
|
|
261
|
+
expect(groups[1]?.dateKey).toBe("2024-02-02");
|
|
262
|
+
expect(groups[2]?.dateKey).toBe("2024-02-01");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("handles a single item", () => {
|
|
266
|
+
const items = [makeItem("2024-06-15", "Jun 15, 2024")];
|
|
267
|
+
const groups = groupByDate(items);
|
|
268
|
+
expect(groups).toHaveLength(1);
|
|
269
|
+
expect(groups[0]?.dateKey).toBe("2024-06-15");
|
|
270
|
+
expect(groups[0]?.items).toHaveLength(1);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("uses the first item's formatted date as the group label", () => {
|
|
274
|
+
const items = [
|
|
275
|
+
makeItem("2024-03-10", "Mar 10, 2024"),
|
|
276
|
+
makeItem("2024-03-10", "March 10"),
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
const groups = groupByDate(items);
|
|
280
|
+
expect(groups).toHaveLength(1);
|
|
281
|
+
// Label comes from first item in the group
|
|
282
|
+
expect(groups[0]?.label).toBe("Mar 10, 2024");
|
|
283
|
+
});
|
|
284
|
+
});
|