@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,45 +1,41 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
FormatSchema,
|
|
4
|
+
StatusSchema,
|
|
5
5
|
RedirectTypeSchema,
|
|
6
6
|
CreatePostSchema,
|
|
7
7
|
UpdatePostSchema,
|
|
8
8
|
parseFormData,
|
|
9
9
|
parseFormDataOptional,
|
|
10
|
-
|
|
10
|
+
validateMediaCount,
|
|
11
11
|
} from "../schemas.js";
|
|
12
12
|
import { z } from "zod";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
describe("PostTypeSchema", () => {
|
|
20
|
-
it("accepts all valid post types", () => {
|
|
21
|
-
for (const type of POST_TYPES) {
|
|
22
|
-
expect(PostTypeSchema.parse(type)).toBe(type);
|
|
13
|
+
import { FORMATS, STATUSES, MAX_MEDIA_ATTACHMENTS } from "../../types.js";
|
|
14
|
+
|
|
15
|
+
describe("FormatSchema", () => {
|
|
16
|
+
it("accepts all valid formats", () => {
|
|
17
|
+
for (const format of FORMATS) {
|
|
18
|
+
expect(FormatSchema.parse(format)).toBe(format);
|
|
23
19
|
}
|
|
24
20
|
});
|
|
25
21
|
|
|
26
|
-
it("rejects invalid
|
|
27
|
-
expect(() =>
|
|
28
|
-
expect(() =>
|
|
29
|
-
expect(() =>
|
|
22
|
+
it("rejects invalid formats", () => {
|
|
23
|
+
expect(() => FormatSchema.parse("invalid")).toThrow();
|
|
24
|
+
expect(() => FormatSchema.parse("")).toThrow();
|
|
25
|
+
expect(() => FormatSchema.parse(123)).toThrow();
|
|
30
26
|
});
|
|
31
27
|
});
|
|
32
28
|
|
|
33
|
-
describe("
|
|
34
|
-
it("accepts all valid
|
|
35
|
-
for (const
|
|
36
|
-
expect(
|
|
29
|
+
describe("StatusSchema", () => {
|
|
30
|
+
it("accepts all valid statuses", () => {
|
|
31
|
+
for (const status of STATUSES) {
|
|
32
|
+
expect(StatusSchema.parse(status)).toBe(status);
|
|
37
33
|
}
|
|
38
34
|
});
|
|
39
35
|
|
|
40
|
-
it("rejects invalid
|
|
41
|
-
expect(() =>
|
|
42
|
-
expect(() =>
|
|
36
|
+
it("rejects invalid statuses", () => {
|
|
37
|
+
expect(() => StatusSchema.parse("public")).toThrow();
|
|
38
|
+
expect(() => StatusSchema.parse("private")).toThrow();
|
|
43
39
|
});
|
|
44
40
|
});
|
|
45
41
|
|
|
@@ -58,22 +54,22 @@ describe("RedirectTypeSchema", () => {
|
|
|
58
54
|
|
|
59
55
|
describe("CreatePostSchema", () => {
|
|
60
56
|
const validPost = {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
format: "note",
|
|
58
|
+
body: "Hello world",
|
|
59
|
+
status: "published",
|
|
64
60
|
};
|
|
65
61
|
|
|
66
62
|
it("accepts a valid post with required fields", () => {
|
|
67
63
|
const result = CreatePostSchema.parse(validPost);
|
|
68
|
-
expect(result.
|
|
69
|
-
expect(result.
|
|
70
|
-
expect(result.
|
|
64
|
+
expect(result.format).toBe("note");
|
|
65
|
+
expect(result.body).toBe("Hello world");
|
|
66
|
+
expect(result.status).toBe("published");
|
|
71
67
|
});
|
|
72
68
|
|
|
73
|
-
it("accepts all
|
|
74
|
-
for (const
|
|
69
|
+
it("accepts all formats", () => {
|
|
70
|
+
for (const format of FORMATS) {
|
|
75
71
|
expect(() =>
|
|
76
|
-
CreatePostSchema.parse({ ...validPost,
|
|
72
|
+
CreatePostSchema.parse({ ...validPost, format }),
|
|
77
73
|
).not.toThrow();
|
|
78
74
|
}
|
|
79
75
|
});
|
|
@@ -89,14 +85,38 @@ describe("CreatePostSchema", () => {
|
|
|
89
85
|
it("accepts valid path format", () => {
|
|
90
86
|
const result = CreatePostSchema.parse({
|
|
91
87
|
...validPost,
|
|
92
|
-
path: "my-post-
|
|
88
|
+
path: "my-post-path",
|
|
89
|
+
});
|
|
90
|
+
expect(result.path).toBe("my-post-path");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("accepts single-character path", () => {
|
|
94
|
+
const result = CreatePostSchema.parse({
|
|
95
|
+
...validPost,
|
|
96
|
+
path: "a",
|
|
93
97
|
});
|
|
94
|
-
expect(result.path).toBe("
|
|
98
|
+
expect(result.path).toBe("a");
|
|
95
99
|
});
|
|
96
100
|
|
|
97
|
-
it("accepts empty path", () => {
|
|
101
|
+
it("accepts empty path (transforms to undefined)", () => {
|
|
98
102
|
const result = CreatePostSchema.parse({ ...validPost, path: "" });
|
|
99
|
-
expect(result.path).
|
|
103
|
+
expect(result.path).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("accepts multi-level path", () => {
|
|
107
|
+
const result = CreatePostSchema.parse({
|
|
108
|
+
...validPost,
|
|
109
|
+
path: "2024/my-post",
|
|
110
|
+
});
|
|
111
|
+
expect(result.path).toBe("2024/my-post");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("accepts deeply nested path", () => {
|
|
115
|
+
const result = CreatePostSchema.parse({
|
|
116
|
+
...validPost,
|
|
117
|
+
path: "2024/01/my-post",
|
|
118
|
+
});
|
|
119
|
+
expect(result.path).toBe("2024/01/my-post");
|
|
100
120
|
});
|
|
101
121
|
|
|
102
122
|
it("rejects invalid path format (uppercase)", () => {
|
|
@@ -111,22 +131,52 @@ describe("CreatePostSchema", () => {
|
|
|
111
131
|
).toThrow();
|
|
112
132
|
});
|
|
113
133
|
|
|
114
|
-
it("
|
|
134
|
+
it("rejects path starting with hyphen", () => {
|
|
135
|
+
expect(() =>
|
|
136
|
+
CreatePostSchema.parse({ ...validPost, path: "-my-post" }),
|
|
137
|
+
).toThrow();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("rejects path ending with hyphen", () => {
|
|
141
|
+
expect(() =>
|
|
142
|
+
CreatePostSchema.parse({ ...validPost, path: "my-post-" }),
|
|
143
|
+
).toThrow();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("rejects path with leading slash", () => {
|
|
147
|
+
expect(() =>
|
|
148
|
+
CreatePostSchema.parse({ ...validPost, path: "/my-post" }),
|
|
149
|
+
).toThrow();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("rejects path with trailing slash", () => {
|
|
153
|
+
expect(() =>
|
|
154
|
+
CreatePostSchema.parse({ ...validPost, path: "my-post/" }),
|
|
155
|
+
).toThrow();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("rejects path with consecutive slashes", () => {
|
|
159
|
+
expect(() =>
|
|
160
|
+
CreatePostSchema.parse({ ...validPost, path: "2024//my-post" }),
|
|
161
|
+
).toThrow();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("accepts valid url", () => {
|
|
115
165
|
const result = CreatePostSchema.parse({
|
|
116
166
|
...validPost,
|
|
117
|
-
|
|
167
|
+
url: "https://example.com",
|
|
118
168
|
});
|
|
119
|
-
expect(result.
|
|
169
|
+
expect(result.url).toBe("https://example.com");
|
|
120
170
|
});
|
|
121
171
|
|
|
122
|
-
it("accepts empty
|
|
123
|
-
const result = CreatePostSchema.parse({ ...validPost,
|
|
124
|
-
expect(result.
|
|
172
|
+
it("accepts empty url", () => {
|
|
173
|
+
const result = CreatePostSchema.parse({ ...validPost, url: "" });
|
|
174
|
+
expect(result.url).toBe("");
|
|
125
175
|
});
|
|
126
176
|
|
|
127
|
-
it("rejects invalid
|
|
177
|
+
it("rejects invalid url", () => {
|
|
128
178
|
expect(() =>
|
|
129
|
-
CreatePostSchema.parse({ ...validPost,
|
|
179
|
+
CreatePostSchema.parse({ ...validPost, url: "not-a-url" }),
|
|
130
180
|
).toThrow();
|
|
131
181
|
});
|
|
132
182
|
|
|
@@ -181,10 +231,84 @@ describe("CreatePostSchema", () => {
|
|
|
181
231
|
).toThrow();
|
|
182
232
|
});
|
|
183
233
|
|
|
184
|
-
it("
|
|
234
|
+
it("accepts featured as boolean", () => {
|
|
235
|
+
const result = CreatePostSchema.parse({ ...validPost, featured: true });
|
|
236
|
+
expect(result.featured).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("accepts featured as 'on' (transforms to true)", () => {
|
|
240
|
+
const result = CreatePostSchema.parse({ ...validPost, featured: "on" });
|
|
241
|
+
expect(result.featured).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("accepts pinned as boolean", () => {
|
|
245
|
+
const result = CreatePostSchema.parse({ ...validPost, pinned: true });
|
|
246
|
+
expect(result.pinned).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("accepts pinned as 'on' (transforms to true)", () => {
|
|
250
|
+
const result = CreatePostSchema.parse({ ...validPost, pinned: "on" });
|
|
251
|
+
expect(result.pinned).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("accepts optional quoteText", () => {
|
|
255
|
+
const result = CreatePostSchema.parse({
|
|
256
|
+
...validPost,
|
|
257
|
+
quoteText: "A wise person once said...",
|
|
258
|
+
});
|
|
259
|
+
expect(result.quoteText).toBe("A wise person once said...");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("accepts optional rating (1-5)", () => {
|
|
263
|
+
for (const rating of [1, 2, 3, 4, 5]) {
|
|
264
|
+
const result = CreatePostSchema.parse({ ...validPost, rating });
|
|
265
|
+
expect(result.rating).toBe(rating);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("rejects rating outside 0-5 range", () => {
|
|
270
|
+
expect(() =>
|
|
271
|
+
CreatePostSchema.parse({ ...validPost, rating: -1 }),
|
|
272
|
+
).toThrow();
|
|
273
|
+
expect(() => CreatePostSchema.parse({ ...validPost, rating: 6 })).toThrow();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("accepts rating 0 (transforms to undefined)", () => {
|
|
277
|
+
const result = CreatePostSchema.parse({ ...validPost, rating: 0 });
|
|
278
|
+
expect(result.rating).toBeUndefined();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("accepts empty string rating (transforms to undefined)", () => {
|
|
282
|
+
const result = CreatePostSchema.parse({ ...validPost, rating: "" });
|
|
283
|
+
expect(result.rating).toBeUndefined();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("accepts optional collectionId as positive integer", () => {
|
|
287
|
+
const result = CreatePostSchema.parse({ ...validPost, collectionId: 42 });
|
|
288
|
+
expect(result.collectionId).toBe(42);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("accepts empty string collectionId (transforms to undefined)", () => {
|
|
292
|
+
const result = CreatePostSchema.parse({ ...validPost, collectionId: "" });
|
|
293
|
+
expect(result.collectionId).toBeUndefined();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("accepts optional replyToId", () => {
|
|
297
|
+
const result = CreatePostSchema.parse({
|
|
298
|
+
...validPost,
|
|
299
|
+
replyToId: "abc123",
|
|
300
|
+
});
|
|
301
|
+
expect(result.replyToId).toBe("abc123");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("only requires format field", () => {
|
|
305
|
+
const result = CreatePostSchema.parse({ format: "note" });
|
|
306
|
+
expect(result.format).toBe("note");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("rejects missing format", () => {
|
|
185
310
|
expect(() => CreatePostSchema.parse({})).toThrow();
|
|
186
|
-
expect(() => CreatePostSchema.parse({
|
|
187
|
-
expect(() => CreatePostSchema.parse({ content: "hello" })).toThrow();
|
|
311
|
+
expect(() => CreatePostSchema.parse({ body: "hello" })).toThrow();
|
|
188
312
|
});
|
|
189
313
|
});
|
|
190
314
|
|
|
@@ -199,13 +323,13 @@ describe("UpdatePostSchema", () => {
|
|
|
199
323
|
expect(result.title).toBe("New Title");
|
|
200
324
|
});
|
|
201
325
|
|
|
202
|
-
it("accepts only
|
|
203
|
-
const result = UpdatePostSchema.parse({
|
|
204
|
-
expect(result.
|
|
326
|
+
it("accepts only format", () => {
|
|
327
|
+
const result = UpdatePostSchema.parse({ format: "link" });
|
|
328
|
+
expect(result.format).toBe("link");
|
|
205
329
|
});
|
|
206
330
|
|
|
207
331
|
it("still validates field types", () => {
|
|
208
|
-
expect(() => UpdatePostSchema.parse({
|
|
332
|
+
expect(() => UpdatePostSchema.parse({ format: "invalid" })).toThrow();
|
|
209
333
|
});
|
|
210
334
|
});
|
|
211
335
|
|
|
@@ -225,8 +349,8 @@ describe("parseFormData", () => {
|
|
|
225
349
|
|
|
226
350
|
it("throws for invalid value", () => {
|
|
227
351
|
const form = new FormData();
|
|
228
|
-
form.set("
|
|
229
|
-
expect(() => parseFormData(form, "
|
|
352
|
+
form.set("format", "invalid-format");
|
|
353
|
+
expect(() => parseFormData(form, "format", FormatSchema)).toThrow();
|
|
230
354
|
});
|
|
231
355
|
});
|
|
232
356
|
|
|
@@ -250,59 +374,37 @@ describe("parseFormDataOptional", () => {
|
|
|
250
374
|
|
|
251
375
|
it("throws for invalid value", () => {
|
|
252
376
|
const form = new FormData();
|
|
253
|
-
form.set("
|
|
254
|
-
expect(() => parseFormDataOptional(form, "
|
|
377
|
+
form.set("format", "invalid");
|
|
378
|
+
expect(() => parseFormDataOptional(form, "format", FormatSchema)).toThrow();
|
|
255
379
|
});
|
|
256
380
|
});
|
|
257
381
|
|
|
258
|
-
describe("
|
|
259
|
-
it("returns null for
|
|
260
|
-
expect(
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("returns null for note with media", () => {
|
|
264
|
-
expect(validateMediaForPostType("note", ["id-1", "id-2"])).toBeNull();
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("returns null for article with media", () => {
|
|
268
|
-
expect(validateMediaForPostType("article", ["id-1"])).toBeNull();
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("returns null for image with at least 1 media", () => {
|
|
272
|
-
expect(validateMediaForPostType("image", ["id-1"])).toBeNull();
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("returns error for image with no media", () => {
|
|
276
|
-
const error = validateMediaForPostType("image", []);
|
|
277
|
-
expect(error).toBe("image posts require at least 1 media attachment");
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("returns null for link with 0 or 1 media", () => {
|
|
281
|
-
expect(validateMediaForPostType("link", [])).toBeNull();
|
|
282
|
-
expect(validateMediaForPostType("link", ["id-1"])).toBeNull();
|
|
382
|
+
describe("validateMediaCount", () => {
|
|
383
|
+
it("returns null for empty media array", () => {
|
|
384
|
+
expect(validateMediaCount([])).toBeNull();
|
|
283
385
|
});
|
|
284
386
|
|
|
285
|
-
it("returns
|
|
286
|
-
const
|
|
287
|
-
expect(
|
|
387
|
+
it("returns null for media within limit", () => {
|
|
388
|
+
const media = Array.from({ length: 5 }, (_, i) => `id-${i}`);
|
|
389
|
+
expect(validateMediaCount(media)).toBeNull();
|
|
288
390
|
});
|
|
289
391
|
|
|
290
|
-
it("returns
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
expect(validateMediaForPostType("page", [])).toBeNull();
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("returns null for quote with media", () => {
|
|
300
|
-
expect(validateMediaForPostType("quote", ["id-1", "id-2"])).toBeNull();
|
|
392
|
+
it("returns null for exactly MAX_MEDIA_ATTACHMENTS", () => {
|
|
393
|
+
const media = Array.from(
|
|
394
|
+
{ length: MAX_MEDIA_ATTACHMENTS },
|
|
395
|
+
(_, i) => `id-${i}`,
|
|
396
|
+
);
|
|
397
|
+
expect(validateMediaCount(media)).toBeNull();
|
|
301
398
|
});
|
|
302
399
|
|
|
303
|
-
it("returns error when exceeding
|
|
304
|
-
const tooMany = Array.from(
|
|
305
|
-
|
|
306
|
-
|
|
400
|
+
it("returns error when exceeding MAX_MEDIA_ATTACHMENTS", () => {
|
|
401
|
+
const tooMany = Array.from(
|
|
402
|
+
{ length: MAX_MEDIA_ATTACHMENTS + 1 },
|
|
403
|
+
(_, i) => `id-${i}`,
|
|
404
|
+
);
|
|
405
|
+
const error = validateMediaCount(tooMany);
|
|
406
|
+
expect(error).toBe(
|
|
407
|
+
`Posts allow at most ${MAX_MEDIA_ATTACHMENTS} media attachments`,
|
|
408
|
+
);
|
|
307
409
|
});
|
|
308
410
|
});
|
|
@@ -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");
|