@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.
Files changed (178) hide show
  1. package/dist/app.js +23 -5
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +5 -6
  7. package/dist/lib/constants.js +1 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/navigation.js +4 -5
  11. package/dist/lib/render.js +1 -1
  12. package/dist/lib/schemas.js +80 -38
  13. package/dist/lib/theme-components.js +8 -11
  14. package/dist/lib/time.js +56 -1
  15. package/dist/lib/timeline.js +119 -0
  16. package/dist/lib/view.js +62 -73
  17. package/dist/routes/api/posts.js +29 -35
  18. package/dist/routes/api/search.js +5 -6
  19. package/dist/routes/api/upload.js +13 -13
  20. package/dist/routes/dash/collections.js +22 -40
  21. package/dist/routes/dash/index.js +2 -2
  22. package/dist/routes/dash/navigation.js +25 -24
  23. package/dist/routes/dash/pages.js +42 -57
  24. package/dist/routes/dash/posts.js +27 -35
  25. package/dist/routes/feed/rss.js +2 -4
  26. package/dist/routes/feed/sitemap.js +10 -7
  27. package/dist/routes/pages/archive.js +12 -11
  28. package/dist/routes/pages/collection.js +11 -5
  29. package/dist/routes/pages/home.js +53 -61
  30. package/dist/routes/pages/page.js +60 -29
  31. package/dist/routes/pages/post.js +5 -12
  32. package/dist/routes/pages/search.js +3 -4
  33. package/dist/services/collection.js +52 -64
  34. package/dist/services/index.js +5 -3
  35. package/dist/services/navigation.js +29 -53
  36. package/dist/services/page.js +80 -0
  37. package/dist/services/post.js +68 -69
  38. package/dist/services/search.js +24 -18
  39. package/dist/theme/components/MediaGallery.js +19 -91
  40. package/dist/theme/components/PageForm.js +15 -15
  41. package/dist/theme/components/PostForm.js +136 -129
  42. package/dist/theme/components/PostList.js +13 -8
  43. package/dist/theme/components/ThreadView.js +3 -3
  44. package/dist/theme/components/TypeBadge.js +3 -14
  45. package/dist/theme/components/VisibilityBadge.js +33 -23
  46. package/dist/theme/components/index.js +0 -2
  47. package/dist/theme/index.js +10 -16
  48. package/dist/theme/layouts/index.js +0 -1
  49. package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
  50. package/dist/themes/threads/index.js +81 -0
  51. package/dist/{theme → themes/threads}/pages/ArchivePage.js +31 -47
  52. package/dist/themes/threads/pages/CollectionPage.js +65 -0
  53. package/dist/{theme → themes/threads}/pages/HomePage.js +4 -5
  54. package/dist/{theme → themes/threads}/pages/PostPage.js +10 -8
  55. package/dist/{theme → themes/threads}/pages/SearchPage.js +8 -8
  56. package/dist/{theme → themes/threads}/pages/SinglePage.js +5 -6
  57. package/dist/{theme/components → themes/threads}/timeline/LinkCard.js +20 -11
  58. package/dist/themes/threads/timeline/NoteCard.js +53 -0
  59. package/dist/themes/threads/timeline/QuoteCard.js +59 -0
  60. package/dist/{theme/components → themes/threads}/timeline/ThreadPreview.js +5 -6
  61. package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
  62. package/dist/{theme/components → themes/threads}/timeline/TimelineItem.js +8 -17
  63. package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
  64. package/dist/themes/threads/timeline/groupByDate.js +22 -0
  65. package/dist/themes/threads/timeline/timelineMore.js +107 -0
  66. package/dist/types.js +24 -40
  67. package/package.json +2 -1
  68. package/src/__tests__/helpers/app.ts +4 -0
  69. package/src/__tests__/helpers/db.ts +51 -74
  70. package/src/app.tsx +27 -6
  71. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  72. package/src/db/migrations/meta/_journal.json +7 -0
  73. package/src/db/schema.ts +63 -46
  74. package/src/i18n/locales/en.po +216 -164
  75. package/src/i18n/locales/en.ts +1 -1
  76. package/src/i18n/locales/zh-Hans.po +216 -164
  77. package/src/i18n/locales/zh-Hans.ts +1 -1
  78. package/src/i18n/locales/zh-Hant.po +216 -164
  79. package/src/i18n/locales/zh-Hant.ts +1 -1
  80. package/src/index.ts +30 -15
  81. package/src/lib/__tests__/excerpt.test.ts +125 -0
  82. package/src/lib/__tests__/schemas.test.ts +166 -105
  83. package/src/lib/__tests__/theme-components.test.ts +4 -25
  84. package/src/lib/__tests__/time.test.ts +62 -0
  85. package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
  86. package/src/lib/__tests__/view.test.ts +217 -67
  87. package/src/lib/constants.ts +1 -4
  88. package/src/lib/excerpt.ts +87 -0
  89. package/src/lib/feed.ts +22 -7
  90. package/src/lib/navigation.ts +6 -7
  91. package/src/lib/render.tsx +1 -1
  92. package/src/lib/schemas.ts +118 -52
  93. package/src/lib/theme-components.ts +10 -13
  94. package/src/lib/time.ts +64 -0
  95. package/src/lib/timeline.ts +170 -0
  96. package/src/lib/view.ts +81 -83
  97. package/src/preset.css +45 -0
  98. package/src/routes/api/__tests__/posts.test.ts +50 -108
  99. package/src/routes/api/__tests__/search.test.ts +2 -3
  100. package/src/routes/api/posts.ts +30 -30
  101. package/src/routes/api/search.ts +4 -4
  102. package/src/routes/api/upload.ts +16 -6
  103. package/src/routes/dash/collections.tsx +18 -40
  104. package/src/routes/dash/index.tsx +2 -2
  105. package/src/routes/dash/navigation.tsx +27 -26
  106. package/src/routes/dash/pages.tsx +45 -60
  107. package/src/routes/dash/posts.tsx +44 -52
  108. package/src/routes/feed/rss.ts +2 -1
  109. package/src/routes/feed/sitemap.ts +14 -4
  110. package/src/routes/pages/archive.tsx +14 -10
  111. package/src/routes/pages/collection.tsx +17 -6
  112. package/src/routes/pages/home.tsx +56 -81
  113. package/src/routes/pages/page.tsx +64 -27
  114. package/src/routes/pages/post.tsx +5 -14
  115. package/src/routes/pages/search.tsx +2 -2
  116. package/src/services/__tests__/collection.test.ts +257 -158
  117. package/src/services/__tests__/media.test.ts +18 -18
  118. package/src/services/__tests__/navigation.test.ts +161 -87
  119. package/src/services/__tests__/post-timeline.test.ts +92 -88
  120. package/src/services/__tests__/post.test.ts +342 -206
  121. package/src/services/__tests__/search.test.ts +19 -25
  122. package/src/services/collection.ts +71 -113
  123. package/src/services/index.ts +9 -8
  124. package/src/services/navigation.ts +38 -71
  125. package/src/services/page.ts +124 -0
  126. package/src/services/post.ts +93 -103
  127. package/src/services/search.ts +38 -27
  128. package/src/styles/components.css +0 -54
  129. package/src/theme/components/MediaGallery.tsx +27 -96
  130. package/src/theme/components/PageForm.tsx +21 -21
  131. package/src/theme/components/PostForm.tsx +122 -118
  132. package/src/theme/components/PostList.tsx +58 -49
  133. package/src/theme/components/ThreadView.tsx +6 -3
  134. package/src/theme/components/TypeBadge.tsx +9 -17
  135. package/src/theme/components/VisibilityBadge.tsx +40 -23
  136. package/src/theme/components/index.ts +0 -13
  137. package/src/theme/index.ts +10 -16
  138. package/src/theme/layouts/index.ts +0 -1
  139. package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
  140. package/src/themes/threads/index.ts +100 -0
  141. package/src/{theme → themes/threads}/pages/ArchivePage.tsx +52 -55
  142. package/src/themes/threads/pages/CollectionPage.tsx +61 -0
  143. package/src/{theme → themes/threads}/pages/HomePage.tsx +5 -6
  144. package/src/{theme → themes/threads}/pages/PostPage.tsx +11 -8
  145. package/src/{theme → themes/threads}/pages/SearchPage.tsx +9 -13
  146. package/src/themes/threads/pages/SinglePage.tsx +23 -0
  147. package/src/themes/threads/style.css +336 -0
  148. package/src/{theme/components → themes/threads}/timeline/LinkCard.tsx +21 -13
  149. package/src/themes/threads/timeline/NoteCard.tsx +58 -0
  150. package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
  151. package/src/{theme/components → themes/threads}/timeline/ThreadPreview.tsx +6 -6
  152. package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
  153. package/src/{theme/components → themes/threads}/timeline/TimelineItem.tsx +9 -20
  154. package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
  155. package/src/themes/threads/timeline/groupByDate.ts +30 -0
  156. package/src/themes/threads/timeline/timelineMore.tsx +130 -0
  157. package/src/types.ts +242 -98
  158. package/dist/routes/api/timeline.js +0 -120
  159. package/dist/theme/components/timeline/ArticleCard.js +0 -46
  160. package/dist/theme/components/timeline/ImageCard.js +0 -83
  161. package/dist/theme/components/timeline/NoteCard.js +0 -34
  162. package/dist/theme/components/timeline/QuoteCard.js +0 -48
  163. package/dist/theme/components/timeline/TimelineFeed.js +0 -46
  164. package/dist/theme/components/timeline/index.js +0 -8
  165. package/dist/theme/layouts/SiteLayout.js +0 -131
  166. package/dist/theme/pages/CollectionPage.js +0 -63
  167. package/dist/theme/pages/index.js +0 -11
  168. package/src/routes/api/timeline.tsx +0 -159
  169. package/src/theme/components/timeline/ArticleCard.tsx +0 -45
  170. package/src/theme/components/timeline/ImageCard.tsx +0 -70
  171. package/src/theme/components/timeline/NoteCard.tsx +0 -34
  172. package/src/theme/components/timeline/QuoteCard.tsx +0 -48
  173. package/src/theme/components/timeline/TimelineFeed.tsx +0 -56
  174. package/src/theme/components/timeline/index.ts +0 -8
  175. package/src/theme/layouts/SiteLayout.tsx +0 -132
  176. package/src/theme/pages/CollectionPage.tsx +0 -60
  177. package/src/theme/pages/SinglePage.tsx +0 -24
  178. 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
- type: "note",
26
- content: "Hello world",
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].content).toBe("Hello world");
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
- type: "note",
44
- content: "with media",
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 visibility", async () => {
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
- type: "note",
77
- content: "featured",
78
- visibility: "featured",
74
+ format: "note",
75
+ body: "published post",
79
76
  });
80
77
  await services.posts.create({
81
- type: "note",
82
- content: "draft",
83
- visibility: "draft",
78
+ format: "note",
79
+ body: "draft post",
80
+ status: "draft",
84
81
  });
85
82
 
86
- const res = await app.request("/api/posts?visibility=draft");
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].visibility).toBe("draft");
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
- type: "note",
100
- content: `post ${i}`,
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
- type: "note",
120
- content: "test post",
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.content).toBe("test post");
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
- type: "note",
139
- content: "with media",
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
- type: "note",
187
- content: "test",
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
- type: "note",
204
- content: "Hello from API",
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.content).toBe("Hello from API");
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
- type: "note",
241
- content: "with images",
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
- type: "note",
313
- content: "test",
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({ type: "invalid-type" }),
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
- type: "note",
360
- content: "original",
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({ content: "updated" }),
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
- type: "note",
378
- content: "original",
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({ content: "updated" }),
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.content).toBe("updated");
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
- type: "note",
399
- content: "test",
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
- type: "note",
438
- content: "test",
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({ content: "updated content" }),
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({ content: "test" }),
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
- type: "note",
482
- content: "test",
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({ type: "invalid-type" }),
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
- type: "note",
502
- content: "test",
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
- type: "note",
518
- content: "to be deleted",
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
- type: "note",
43
- content: "Testing search functionality in jant",
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");
@@ -3,13 +3,13 @@
3
3
  */
4
4
 
5
5
  import { Hono } from "hono";
6
- import type { Bindings, PostType, Visibility, Media } from "../../types.js";
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
- validateMediaForPostType,
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 type = c.req.query("type") as PostType | undefined;
63
- const visibility = c.req.query("visibility") as Visibility | undefined;
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
- type,
69
- visibility: visibility ? [visibility] : ["featured", "quiet"],
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 for post type
134
+ // Validate media count
135
135
  if (body.mediaIds) {
136
- const mediaError = validateMediaForPostType(body.type, body.mediaIds);
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
- type: body.type,
151
+ format: body.format,
152
152
  title: body.title,
153
- content: body.content,
154
- visibility: body.visibility,
155
- sourceUrl: body.sourceUrl || undefined,
156
- sourceName: body.sourceName,
157
- path: body.path || undefined,
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 for post type if mediaIds is provided
208
+ // Validate media count if mediaIds is provided
205
209
  if (body.mediaIds !== undefined) {
206
- // Need the post type — use the new type if provided, else fetch existing
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
- type: body.type,
225
+ format: body.format,
230
226
  title: body.title,
231
- content: body.content,
232
- visibility: body.visibility,
233
- sourceUrl: body.sourceUrl,
234
- sourceName: body.sourceName,
235
- path: body.path,
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
 
@@ -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
- visibility: ["featured", "quiet"],
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
- type: r.post.type,
39
+ format: r.post.format,
40
40
  title: r.post.title,
41
- path: r.post.path,
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
  });
@@ -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, dsSignals } from "../../lib/sse.js";
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 dsSignals({ _uploadError: "Storage not configured" });
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 dsSignals({ _uploadError: "No file provided" });
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 dsSignals({ _uploadError: "File type not allowed" });
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 dsSignals({ _uploadError: "File too large (max 10MB)" });
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
  }