@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
|
@@ -22,16 +22,15 @@ describe("Posts API Routes", () => {
|
|
|
22
22
|
app.route("/api/posts", postsApiRoutes);
|
|
23
23
|
|
|
24
24
|
await services.posts.create({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
visibility: "featured",
|
|
25
|
+
format: "note",
|
|
26
|
+
body: "Hello world",
|
|
28
27
|
});
|
|
29
28
|
|
|
30
29
|
const res = await app.request("/api/posts");
|
|
31
30
|
const body = await res.json();
|
|
32
31
|
|
|
33
32
|
expect(body.posts).toHaveLength(1);
|
|
34
|
-
expect(body.posts[0].
|
|
33
|
+
expect(body.posts[0].body).toBe("Hello world");
|
|
35
34
|
expect(body.posts[0].sqid).toBeTruthy();
|
|
36
35
|
});
|
|
37
36
|
|
|
@@ -40,9 +39,8 @@ describe("Posts API Routes", () => {
|
|
|
40
39
|
app.route("/api/posts", postsApiRoutes);
|
|
41
40
|
|
|
42
41
|
const post = await services.posts.create({
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
visibility: "featured",
|
|
42
|
+
format: "note",
|
|
43
|
+
body: "with media",
|
|
46
44
|
});
|
|
47
45
|
|
|
48
46
|
const media = await services.media.create({
|
|
@@ -68,26 +66,25 @@ describe("Posts API Routes", () => {
|
|
|
68
66
|
expect(body.posts[0].mediaAttachments[0].position).toBe(0);
|
|
69
67
|
});
|
|
70
68
|
|
|
71
|
-
it("filters by
|
|
69
|
+
it("filters by status", async () => {
|
|
72
70
|
const { app, services } = createTestApp();
|
|
73
71
|
app.route("/api/posts", postsApiRoutes);
|
|
74
72
|
|
|
75
73
|
await services.posts.create({
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
visibility: "featured",
|
|
74
|
+
format: "note",
|
|
75
|
+
body: "published post",
|
|
79
76
|
});
|
|
80
77
|
await services.posts.create({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
format: "note",
|
|
79
|
+
body: "draft post",
|
|
80
|
+
status: "draft",
|
|
84
81
|
});
|
|
85
82
|
|
|
86
|
-
const res = await app.request("/api/posts?
|
|
83
|
+
const res = await app.request("/api/posts?status=draft");
|
|
87
84
|
const body = await res.json();
|
|
88
85
|
|
|
89
86
|
expect(body.posts).toHaveLength(1);
|
|
90
|
-
expect(body.posts[0].
|
|
87
|
+
expect(body.posts[0].status).toBe("draft");
|
|
91
88
|
});
|
|
92
89
|
|
|
93
90
|
it("supports limit parameter", async () => {
|
|
@@ -96,9 +93,8 @@ describe("Posts API Routes", () => {
|
|
|
96
93
|
|
|
97
94
|
for (let i = 0; i < 5; i++) {
|
|
98
95
|
await services.posts.create({
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
visibility: "featured",
|
|
96
|
+
format: "note",
|
|
97
|
+
body: `post ${i}`,
|
|
102
98
|
});
|
|
103
99
|
}
|
|
104
100
|
|
|
@@ -116,9 +112,8 @@ describe("Posts API Routes", () => {
|
|
|
116
112
|
app.route("/api/posts", postsApiRoutes);
|
|
117
113
|
|
|
118
114
|
const post = await services.posts.create({
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
visibility: "featured",
|
|
115
|
+
format: "note",
|
|
116
|
+
body: "test post",
|
|
122
117
|
});
|
|
123
118
|
const id = sqid.encode(post.id);
|
|
124
119
|
|
|
@@ -126,7 +121,7 @@ describe("Posts API Routes", () => {
|
|
|
126
121
|
expect(res.status).toBe(200);
|
|
127
122
|
|
|
128
123
|
const body = await res.json();
|
|
129
|
-
expect(body.
|
|
124
|
+
expect(body.body).toBe("test post");
|
|
130
125
|
expect(body.sqid).toBe(id);
|
|
131
126
|
});
|
|
132
127
|
|
|
@@ -135,9 +130,8 @@ describe("Posts API Routes", () => {
|
|
|
135
130
|
app.route("/api/posts", postsApiRoutes);
|
|
136
131
|
|
|
137
132
|
const post = await services.posts.create({
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
visibility: "featured",
|
|
133
|
+
format: "note",
|
|
134
|
+
body: "with media",
|
|
141
135
|
});
|
|
142
136
|
|
|
143
137
|
const media = await services.media.create({
|
|
@@ -183,9 +177,8 @@ describe("Posts API Routes", () => {
|
|
|
183
177
|
method: "POST",
|
|
184
178
|
headers: { "Content-Type": "application/json" },
|
|
185
179
|
body: JSON.stringify({
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
visibility: "quiet",
|
|
180
|
+
format: "note",
|
|
181
|
+
body: "test",
|
|
189
182
|
}),
|
|
190
183
|
});
|
|
191
184
|
|
|
@@ -200,16 +193,15 @@ describe("Posts API Routes", () => {
|
|
|
200
193
|
method: "POST",
|
|
201
194
|
headers: { "Content-Type": "application/json" },
|
|
202
195
|
body: JSON.stringify({
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
visibility: "quiet",
|
|
196
|
+
format: "note",
|
|
197
|
+
body: "Hello from API",
|
|
206
198
|
}),
|
|
207
199
|
});
|
|
208
200
|
|
|
209
201
|
expect(res.status).toBe(201);
|
|
210
202
|
|
|
211
203
|
const body = await res.json();
|
|
212
|
-
expect(body.
|
|
204
|
+
expect(body.body).toBe("Hello from API");
|
|
213
205
|
expect(body.sqid).toBeTruthy();
|
|
214
206
|
expect(body.mediaAttachments).toEqual([]);
|
|
215
207
|
});
|
|
@@ -237,9 +229,8 @@ describe("Posts API Routes", () => {
|
|
|
237
229
|
method: "POST",
|
|
238
230
|
headers: { "Content-Type": "application/json" },
|
|
239
231
|
body: JSON.stringify({
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
visibility: "quiet",
|
|
232
|
+
format: "note",
|
|
233
|
+
body: "with images",
|
|
243
234
|
mediaIds: [m1.id, m2.id],
|
|
244
235
|
}),
|
|
245
236
|
});
|
|
@@ -253,54 +244,6 @@ describe("Posts API Routes", () => {
|
|
|
253
244
|
expect(body.mediaAttachments[1].position).toBe(1);
|
|
254
245
|
});
|
|
255
246
|
|
|
256
|
-
it("returns 400 for image type without media", async () => {
|
|
257
|
-
const { app } = createTestApp({ authenticated: true });
|
|
258
|
-
app.route("/api/posts", postsApiRoutes);
|
|
259
|
-
|
|
260
|
-
const res = await app.request("/api/posts", {
|
|
261
|
-
method: "POST",
|
|
262
|
-
headers: { "Content-Type": "application/json" },
|
|
263
|
-
body: JSON.stringify({
|
|
264
|
-
type: "image",
|
|
265
|
-
content: "should fail",
|
|
266
|
-
visibility: "quiet",
|
|
267
|
-
mediaIds: [],
|
|
268
|
-
}),
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
expect(res.status).toBe(400);
|
|
272
|
-
const body = await res.json();
|
|
273
|
-
expect(body.error).toContain("image posts require at least 1");
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("returns 400 for page type with media", async () => {
|
|
277
|
-
const { app, services } = createTestApp({ authenticated: true });
|
|
278
|
-
app.route("/api/posts", postsApiRoutes);
|
|
279
|
-
|
|
280
|
-
const m1 = await services.media.create({
|
|
281
|
-
filename: "a.jpg",
|
|
282
|
-
originalName: "a.jpg",
|
|
283
|
-
mimeType: "image/jpeg",
|
|
284
|
-
size: 1024,
|
|
285
|
-
storageKey: "media/2025/01/a.jpg",
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const res = await app.request("/api/posts", {
|
|
289
|
-
method: "POST",
|
|
290
|
-
headers: { "Content-Type": "application/json" },
|
|
291
|
-
body: JSON.stringify({
|
|
292
|
-
type: "page",
|
|
293
|
-
content: "test",
|
|
294
|
-
visibility: "quiet",
|
|
295
|
-
mediaIds: [m1.id],
|
|
296
|
-
}),
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
expect(res.status).toBe(400);
|
|
300
|
-
const body = await res.json();
|
|
301
|
-
expect(body.error).toContain("page posts do not allow");
|
|
302
|
-
});
|
|
303
|
-
|
|
304
247
|
it("returns 400 for invalid media IDs", async () => {
|
|
305
248
|
const { app } = createTestApp({ authenticated: true });
|
|
306
249
|
app.route("/api/posts", postsApiRoutes);
|
|
@@ -309,9 +252,8 @@ describe("Posts API Routes", () => {
|
|
|
309
252
|
method: "POST",
|
|
310
253
|
headers: { "Content-Type": "application/json" },
|
|
311
254
|
body: JSON.stringify({
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
visibility: "quiet",
|
|
255
|
+
format: "note",
|
|
256
|
+
body: "test",
|
|
315
257
|
mediaIds: ["nonexistent-id"],
|
|
316
258
|
}),
|
|
317
259
|
});
|
|
@@ -328,7 +270,7 @@ describe("Posts API Routes", () => {
|
|
|
328
270
|
const res = await app.request("/api/posts", {
|
|
329
271
|
method: "POST",
|
|
330
272
|
headers: { "Content-Type": "application/json" },
|
|
331
|
-
body: JSON.stringify({
|
|
273
|
+
body: JSON.stringify({ format: "invalid-type" }),
|
|
332
274
|
});
|
|
333
275
|
|
|
334
276
|
expect(res.status).toBe(400);
|
|
@@ -356,14 +298,14 @@ describe("Posts API Routes", () => {
|
|
|
356
298
|
app.route("/api/posts", postsApiRoutes);
|
|
357
299
|
|
|
358
300
|
const post = await services.posts.create({
|
|
359
|
-
|
|
360
|
-
|
|
301
|
+
format: "note",
|
|
302
|
+
body: "original",
|
|
361
303
|
});
|
|
362
304
|
|
|
363
305
|
const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
|
|
364
306
|
method: "PUT",
|
|
365
307
|
headers: { "Content-Type": "application/json" },
|
|
366
|
-
body: JSON.stringify({
|
|
308
|
+
body: JSON.stringify({ body: "updated" }),
|
|
367
309
|
});
|
|
368
310
|
|
|
369
311
|
expect(res.status).toBe(401);
|
|
@@ -374,19 +316,19 @@ describe("Posts API Routes", () => {
|
|
|
374
316
|
app.route("/api/posts", postsApiRoutes);
|
|
375
317
|
|
|
376
318
|
const post = await services.posts.create({
|
|
377
|
-
|
|
378
|
-
|
|
319
|
+
format: "note",
|
|
320
|
+
body: "original",
|
|
379
321
|
});
|
|
380
322
|
|
|
381
323
|
const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
|
|
382
324
|
method: "PUT",
|
|
383
325
|
headers: { "Content-Type": "application/json" },
|
|
384
|
-
body: JSON.stringify({
|
|
326
|
+
body: JSON.stringify({ body: "updated" }),
|
|
385
327
|
});
|
|
386
328
|
|
|
387
329
|
expect(res.status).toBe(200);
|
|
388
330
|
const body = await res.json();
|
|
389
|
-
expect(body.
|
|
331
|
+
expect(body.body).toBe("updated");
|
|
390
332
|
expect(body.mediaAttachments).toEqual([]);
|
|
391
333
|
});
|
|
392
334
|
|
|
@@ -395,8 +337,8 @@ describe("Posts API Routes", () => {
|
|
|
395
337
|
app.route("/api/posts", postsApiRoutes);
|
|
396
338
|
|
|
397
339
|
const post = await services.posts.create({
|
|
398
|
-
|
|
399
|
-
|
|
340
|
+
format: "note",
|
|
341
|
+
body: "test",
|
|
400
342
|
});
|
|
401
343
|
|
|
402
344
|
const m1 = await services.media.create({
|
|
@@ -434,8 +376,8 @@ describe("Posts API Routes", () => {
|
|
|
434
376
|
app.route("/api/posts", postsApiRoutes);
|
|
435
377
|
|
|
436
378
|
const post = await services.posts.create({
|
|
437
|
-
|
|
438
|
-
|
|
379
|
+
format: "note",
|
|
380
|
+
body: "test",
|
|
439
381
|
});
|
|
440
382
|
|
|
441
383
|
const m1 = await services.media.create({
|
|
@@ -451,7 +393,7 @@ describe("Posts API Routes", () => {
|
|
|
451
393
|
const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
|
|
452
394
|
method: "PUT",
|
|
453
395
|
headers: { "Content-Type": "application/json" },
|
|
454
|
-
body: JSON.stringify({
|
|
396
|
+
body: JSON.stringify({ body: "updated content" }),
|
|
455
397
|
});
|
|
456
398
|
|
|
457
399
|
expect(res.status).toBe(200);
|
|
@@ -467,7 +409,7 @@ describe("Posts API Routes", () => {
|
|
|
467
409
|
const res = await app.request(`/api/posts/${sqid.encode(9999)}`, {
|
|
468
410
|
method: "PUT",
|
|
469
411
|
headers: { "Content-Type": "application/json" },
|
|
470
|
-
body: JSON.stringify({
|
|
412
|
+
body: JSON.stringify({ body: "test" }),
|
|
471
413
|
});
|
|
472
414
|
|
|
473
415
|
expect(res.status).toBe(404);
|
|
@@ -478,14 +420,14 @@ describe("Posts API Routes", () => {
|
|
|
478
420
|
app.route("/api/posts", postsApiRoutes);
|
|
479
421
|
|
|
480
422
|
const post = await services.posts.create({
|
|
481
|
-
|
|
482
|
-
|
|
423
|
+
format: "note",
|
|
424
|
+
body: "test",
|
|
483
425
|
});
|
|
484
426
|
|
|
485
427
|
const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
|
|
486
428
|
method: "PUT",
|
|
487
429
|
headers: { "Content-Type": "application/json" },
|
|
488
|
-
body: JSON.stringify({
|
|
430
|
+
body: JSON.stringify({ format: "invalid-type" }),
|
|
489
431
|
});
|
|
490
432
|
|
|
491
433
|
expect(res.status).toBe(400);
|
|
@@ -498,8 +440,8 @@ describe("Posts API Routes", () => {
|
|
|
498
440
|
app.route("/api/posts", postsApiRoutes);
|
|
499
441
|
|
|
500
442
|
const post = await services.posts.create({
|
|
501
|
-
|
|
502
|
-
|
|
443
|
+
format: "note",
|
|
444
|
+
body: "test",
|
|
503
445
|
});
|
|
504
446
|
|
|
505
447
|
const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
|
|
@@ -514,8 +456,8 @@ describe("Posts API Routes", () => {
|
|
|
514
456
|
app.route("/api/posts", postsApiRoutes);
|
|
515
457
|
|
|
516
458
|
const post = await services.posts.create({
|
|
517
|
-
|
|
518
|
-
|
|
459
|
+
format: "note",
|
|
460
|
+
body: "to be deleted",
|
|
519
461
|
});
|
|
520
462
|
|
|
521
463
|
const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
|
|
@@ -39,9 +39,8 @@ describe("Search API Routes", () => {
|
|
|
39
39
|
app.route("/api/search", searchApiRoutes);
|
|
40
40
|
|
|
41
41
|
await services.posts.create({
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
visibility: "featured",
|
|
42
|
+
format: "note",
|
|
43
|
+
body: "Testing search functionality in jant",
|
|
45
44
|
});
|
|
46
45
|
|
|
47
46
|
const res = await app.request("/api/search?q=jant");
|
package/src/routes/api/posts.ts
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import type { Bindings,
|
|
6
|
+
import type { Bindings, Format, Status, Media } from "../../types.js";
|
|
7
7
|
import type { AppVariables } from "../../app.js";
|
|
8
8
|
import * as sqid from "../../lib/sqid.js";
|
|
9
9
|
import {
|
|
10
10
|
CreatePostSchema,
|
|
11
11
|
UpdatePostSchema,
|
|
12
|
-
|
|
12
|
+
validateMediaCount,
|
|
13
13
|
} from "../../lib/schemas.js";
|
|
14
14
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
15
15
|
import {
|
|
@@ -59,14 +59,14 @@ function toMediaAttachment(
|
|
|
59
59
|
|
|
60
60
|
// List posts
|
|
61
61
|
postsApiRoutes.get("/", async (c) => {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
62
|
+
const format = c.req.query("format") as Format | undefined;
|
|
63
|
+
const status = c.req.query("status") as Status | undefined;
|
|
64
64
|
const cursor = c.req.query("cursor");
|
|
65
65
|
const limit = parseInt(c.req.query("limit") ?? "100", 10);
|
|
66
66
|
|
|
67
67
|
const posts = await c.var.services.posts.list({
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
format,
|
|
69
|
+
status: status ?? "published",
|
|
70
70
|
cursor: cursor ? (sqid.decode(cursor) ?? undefined) : undefined,
|
|
71
71
|
limit,
|
|
72
72
|
});
|
|
@@ -131,9 +131,9 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
131
131
|
|
|
132
132
|
const body = parseResult.data;
|
|
133
133
|
|
|
134
|
-
// Validate media
|
|
134
|
+
// Validate media count
|
|
135
135
|
if (body.mediaIds) {
|
|
136
|
-
const mediaError =
|
|
136
|
+
const mediaError = validateMediaCount(body.mediaIds);
|
|
137
137
|
if (mediaError) {
|
|
138
138
|
return c.json({ error: mediaError }, 400);
|
|
139
139
|
}
|
|
@@ -148,13 +148,17 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
const post = await c.var.services.posts.create({
|
|
151
|
-
|
|
151
|
+
format: body.format,
|
|
152
152
|
title: body.title,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
body: body.body,
|
|
154
|
+
slug: body.slug || undefined,
|
|
155
|
+
status: body.status,
|
|
156
|
+
featured: body.featured,
|
|
157
|
+
pinned: body.pinned,
|
|
158
|
+
url: body.url || undefined,
|
|
159
|
+
quoteText: body.quoteText,
|
|
160
|
+
rating: body.rating || undefined,
|
|
161
|
+
collectionId: body.collectionId || undefined,
|
|
158
162
|
replyToId: body.replyToId
|
|
159
163
|
? (sqid.decode(body.replyToId) ?? undefined)
|
|
160
164
|
: undefined,
|
|
@@ -201,17 +205,9 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
201
205
|
|
|
202
206
|
const body = parseResult.data;
|
|
203
207
|
|
|
204
|
-
// Validate media
|
|
208
|
+
// Validate media count if mediaIds is provided
|
|
205
209
|
if (body.mediaIds !== undefined) {
|
|
206
|
-
|
|
207
|
-
let postType = body.type;
|
|
208
|
-
if (!postType) {
|
|
209
|
-
const existing = await c.var.services.posts.getById(id);
|
|
210
|
-
if (!existing) return c.json({ error: "Not found" }, 404);
|
|
211
|
-
postType = existing.type;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const mediaError = validateMediaForPostType(postType, body.mediaIds);
|
|
210
|
+
const mediaError = validateMediaCount(body.mediaIds);
|
|
215
211
|
if (mediaError) {
|
|
216
212
|
return c.json({ error: mediaError }, 400);
|
|
217
213
|
}
|
|
@@ -226,13 +222,17 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
226
222
|
}
|
|
227
223
|
|
|
228
224
|
const post = await c.var.services.posts.update(id, {
|
|
229
|
-
|
|
225
|
+
format: body.format,
|
|
230
226
|
title: body.title,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
227
|
+
body: body.body,
|
|
228
|
+
slug: body.slug,
|
|
229
|
+
status: body.status,
|
|
230
|
+
featured: body.featured,
|
|
231
|
+
pinned: body.pinned,
|
|
232
|
+
url: body.url,
|
|
233
|
+
quoteText: body.quoteText,
|
|
234
|
+
rating: body.rating || undefined,
|
|
235
|
+
collectionId: body.collectionId || undefined,
|
|
236
236
|
publishedAt: body.publishedAt,
|
|
237
237
|
});
|
|
238
238
|
|
package/src/routes/api/search.ts
CHANGED
|
@@ -29,19 +29,19 @@ searchApiRoutes.get("/", async (c) => {
|
|
|
29
29
|
try {
|
|
30
30
|
const results = await c.var.services.search.search(query, {
|
|
31
31
|
limit,
|
|
32
|
-
|
|
32
|
+
status: ["published"],
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
return c.json({
|
|
36
36
|
query,
|
|
37
37
|
results: results.map((r) => ({
|
|
38
38
|
id: sqid.encode(r.post.id),
|
|
39
|
-
|
|
39
|
+
format: r.post.format,
|
|
40
40
|
title: r.post.title,
|
|
41
|
-
|
|
41
|
+
slug: r.post.slug,
|
|
42
42
|
snippet: r.snippet,
|
|
43
43
|
publishedAt: r.post.publishedAt,
|
|
44
|
-
url: `/p/${sqid.encode(r.post.id)}`,
|
|
44
|
+
url: r.post.slug ? `/${r.post.slug}` : `/p/${sqid.encode(r.post.id)}`,
|
|
45
45
|
})),
|
|
46
46
|
count: results.length,
|
|
47
47
|
});
|
package/src/routes/api/upload.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Supports both JSON and SSE (Datastar) responses.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { Hono } from "hono";
|
|
8
|
+
import { Hono, type Context } from "hono";
|
|
9
9
|
import { html } from "hono/html";
|
|
10
10
|
import { uuidv7 } from "uuidv7";
|
|
11
11
|
import type { Bindings } from "../../types.js";
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
getImageUrl,
|
|
17
17
|
getPublicUrlForProvider,
|
|
18
18
|
} from "../../lib/image.js";
|
|
19
|
-
import { sse
|
|
19
|
+
import { sse } from "../../lib/sse.js";
|
|
20
20
|
|
|
21
21
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
22
22
|
|
|
@@ -118,12 +118,22 @@ function wantsSSE(c: {
|
|
|
118
118
|
return accept.includes("text/event-stream");
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Return an SSE error response that removes the upload placeholder and shows a toast
|
|
123
|
+
*/
|
|
124
|
+
function sseUploadError(c: Context<Env>, message: string): Response {
|
|
125
|
+
return sse(c, async (stream) => {
|
|
126
|
+
await stream.remove("#upload-placeholder");
|
|
127
|
+
await stream.toast(message, "error");
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
121
131
|
// Upload a file
|
|
122
132
|
uploadApiRoutes.post("/", async (c) => {
|
|
123
133
|
const storage = c.var.storage;
|
|
124
134
|
if (!storage) {
|
|
125
135
|
if (wantsSSE(c)) {
|
|
126
|
-
return
|
|
136
|
+
return sseUploadError(c, "Storage not configured");
|
|
127
137
|
}
|
|
128
138
|
return c.json({ error: "Storage not configured" }, 500);
|
|
129
139
|
}
|
|
@@ -133,7 +143,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
133
143
|
|
|
134
144
|
if (!file) {
|
|
135
145
|
if (wantsSSE(c)) {
|
|
136
|
-
return
|
|
146
|
+
return sseUploadError(c, "No file provided");
|
|
137
147
|
}
|
|
138
148
|
return c.json({ error: "No file provided" }, 400);
|
|
139
149
|
}
|
|
@@ -148,7 +158,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
148
158
|
];
|
|
149
159
|
if (!allowedTypes.includes(file.type)) {
|
|
150
160
|
if (wantsSSE(c)) {
|
|
151
|
-
return
|
|
161
|
+
return sseUploadError(c, "File type not allowed");
|
|
152
162
|
}
|
|
153
163
|
return c.json({ error: "File type not allowed" }, 400);
|
|
154
164
|
}
|
|
@@ -157,7 +167,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
157
167
|
const maxSize = 10 * 1024 * 1024;
|
|
158
168
|
if (file.size > maxSize) {
|
|
159
169
|
if (wantsSSE(c)) {
|
|
160
|
-
return
|
|
170
|
+
return sseUploadError(c, "File too large (max 10MB)");
|
|
161
171
|
}
|
|
162
172
|
return c.json({ error: "File too large (max 10MB)" }, 400);
|
|
163
173
|
}
|