@jant/core 0.3.23 → 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 (169) hide show
  1. package/dist/app.js +4 -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 +3 -3
  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 +61 -72
  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/themes/threads/ThreadsSiteLayout.js +172 -0
  47. package/dist/themes/threads/index.js +81 -0
  48. package/dist/themes/{minimal → threads}/pages/ArchivePage.js +32 -47
  49. package/dist/themes/threads/pages/CollectionPage.js +65 -0
  50. package/dist/themes/{minimal → threads}/pages/HomePage.js +3 -3
  51. package/dist/themes/{minimal → threads}/pages/PostPage.js +12 -9
  52. package/dist/themes/{minimal → threads}/pages/SearchPage.js +13 -14
  53. package/dist/themes/{minimal → threads}/pages/SinglePage.js +4 -4
  54. package/dist/themes/threads/timeline/LinkCard.js +68 -0
  55. package/dist/themes/threads/timeline/NoteCard.js +53 -0
  56. package/dist/themes/threads/timeline/QuoteCard.js +59 -0
  57. package/dist/themes/{minimal → threads}/timeline/ThreadPreview.js +17 -13
  58. package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
  59. package/dist/themes/{minimal → threads}/timeline/TimelineItem.js +8 -16
  60. package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
  61. package/dist/themes/threads/timeline/groupByDate.js +22 -0
  62. package/dist/themes/threads/timeline/timelineMore.js +107 -0
  63. package/dist/types.js +24 -40
  64. package/package.json +2 -1
  65. package/src/__tests__/helpers/app.ts +4 -0
  66. package/src/__tests__/helpers/db.ts +51 -74
  67. package/src/app.tsx +4 -6
  68. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  69. package/src/db/migrations/meta/_journal.json +7 -0
  70. package/src/db/schema.ts +63 -46
  71. package/src/i18n/locales/en.po +216 -164
  72. package/src/i18n/locales/en.ts +1 -1
  73. package/src/i18n/locales/zh-Hans.po +216 -164
  74. package/src/i18n/locales/zh-Hans.ts +1 -1
  75. package/src/i18n/locales/zh-Hant.po +216 -164
  76. package/src/i18n/locales/zh-Hant.ts +1 -1
  77. package/src/index.ts +28 -12
  78. package/src/lib/__tests__/excerpt.test.ts +125 -0
  79. package/src/lib/__tests__/schemas.test.ts +166 -105
  80. package/src/lib/__tests__/theme-components.test.ts +4 -25
  81. package/src/lib/__tests__/time.test.ts +62 -0
  82. package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
  83. package/src/lib/__tests__/view.test.ts +199 -51
  84. package/src/lib/constants.ts +1 -4
  85. package/src/lib/excerpt.ts +87 -0
  86. package/src/lib/feed.ts +22 -7
  87. package/src/lib/navigation.ts +6 -7
  88. package/src/lib/render.tsx +1 -1
  89. package/src/lib/schemas.ts +118 -52
  90. package/src/lib/theme-components.ts +10 -13
  91. package/src/lib/time.ts +64 -0
  92. package/src/lib/timeline.ts +170 -0
  93. package/src/lib/view.ts +80 -82
  94. package/src/preset.css +45 -0
  95. package/src/routes/api/__tests__/posts.test.ts +50 -108
  96. package/src/routes/api/__tests__/search.test.ts +2 -3
  97. package/src/routes/api/posts.ts +30 -30
  98. package/src/routes/api/search.ts +4 -4
  99. package/src/routes/api/upload.ts +16 -6
  100. package/src/routes/dash/collections.tsx +18 -40
  101. package/src/routes/dash/index.tsx +2 -2
  102. package/src/routes/dash/navigation.tsx +27 -26
  103. package/src/routes/dash/pages.tsx +45 -60
  104. package/src/routes/dash/posts.tsx +44 -52
  105. package/src/routes/feed/rss.ts +2 -1
  106. package/src/routes/feed/sitemap.ts +14 -4
  107. package/src/routes/pages/archive.tsx +14 -10
  108. package/src/routes/pages/collection.tsx +17 -6
  109. package/src/routes/pages/home.tsx +56 -81
  110. package/src/routes/pages/page.tsx +64 -27
  111. package/src/routes/pages/post.tsx +5 -14
  112. package/src/routes/pages/search.tsx +2 -2
  113. package/src/services/__tests__/collection.test.ts +257 -158
  114. package/src/services/__tests__/media.test.ts +18 -18
  115. package/src/services/__tests__/navigation.test.ts +161 -87
  116. package/src/services/__tests__/post-timeline.test.ts +92 -88
  117. package/src/services/__tests__/post.test.ts +342 -206
  118. package/src/services/__tests__/search.test.ts +19 -25
  119. package/src/services/collection.ts +71 -113
  120. package/src/services/index.ts +9 -8
  121. package/src/services/navigation.ts +38 -71
  122. package/src/services/page.ts +124 -0
  123. package/src/services/post.ts +93 -103
  124. package/src/services/search.ts +38 -27
  125. package/src/theme/components/MediaGallery.tsx +27 -96
  126. package/src/theme/components/PageForm.tsx +21 -21
  127. package/src/theme/components/PostForm.tsx +122 -118
  128. package/src/theme/components/PostList.tsx +58 -49
  129. package/src/theme/components/ThreadView.tsx +6 -3
  130. package/src/theme/components/TypeBadge.tsx +9 -17
  131. package/src/theme/components/VisibilityBadge.tsx +40 -23
  132. package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
  133. package/src/themes/{minimal → threads}/index.ts +30 -13
  134. package/src/themes/{minimal → threads}/pages/ArchivePage.tsx +53 -53
  135. package/src/themes/threads/pages/CollectionPage.tsx +61 -0
  136. package/src/themes/{minimal → threads}/pages/HomePage.tsx +3 -3
  137. package/src/themes/{minimal → threads}/pages/PostPage.tsx +12 -8
  138. package/src/themes/{minimal → threads}/pages/SearchPage.tsx +15 -13
  139. package/src/themes/{minimal → threads}/pages/SinglePage.tsx +4 -4
  140. package/src/themes/threads/style.css +336 -0
  141. package/src/themes/threads/timeline/LinkCard.tsx +67 -0
  142. package/src/themes/threads/timeline/NoteCard.tsx +58 -0
  143. package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
  144. package/src/themes/{minimal → threads}/timeline/ThreadPreview.tsx +15 -13
  145. package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
  146. package/src/themes/{minimal → threads}/timeline/TimelineItem.tsx +9 -17
  147. package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
  148. package/src/themes/threads/timeline/groupByDate.ts +30 -0
  149. package/src/themes/threads/timeline/timelineMore.tsx +130 -0
  150. package/src/types.ts +242 -98
  151. package/dist/routes/api/timeline.js +0 -120
  152. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  153. package/dist/themes/minimal/index.js +0 -65
  154. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  155. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  156. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  157. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  158. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  159. package/dist/themes/minimal/timeline/QuoteCard.js +0 -48
  160. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  161. package/src/routes/api/timeline.tsx +0 -159
  162. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  163. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  164. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  165. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  166. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  167. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  168. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  169. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
@@ -2,7 +2,7 @@
2
2
  * Posts API Routes
3
3
  */ import { Hono } from "hono";
4
4
  import * as sqid from "../../lib/sqid.js";
5
- import { CreatePostSchema, UpdatePostSchema, validateMediaForPostType } from "../../lib/schemas.js";
5
+ import { CreatePostSchema, UpdatePostSchema, validateMediaCount } from "../../lib/schemas.js";
6
6
  import { requireAuthApi } from "../../middleware/auth.js";
7
7
  import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "../../lib/image.js";
8
8
  export const postsApiRoutes = new Hono();
@@ -31,18 +31,13 @@ export const postsApiRoutes = new Hono();
31
31
  }
32
32
  // List posts
33
33
  postsApiRoutes.get("/", async (c)=>{
34
- const type = c.req.query("type");
35
- const visibility = c.req.query("visibility");
34
+ const format = c.req.query("format");
35
+ const status = c.req.query("status");
36
36
  const cursor = c.req.query("cursor");
37
37
  const limit = parseInt(c.req.query("limit") ?? "100", 10);
38
38
  const posts = await c.var.services.posts.list({
39
- type,
40
- visibility: visibility ? [
41
- visibility
42
- ] : [
43
- "featured",
44
- "quiet"
45
- ],
39
+ format,
40
+ status: status ?? "published",
46
41
  cursor: cursor ? sqid.decode(cursor) ?? undefined : undefined,
47
42
  limit
48
43
  });
@@ -93,9 +88,9 @@ postsApiRoutes.post("/", requireAuthApi(), async (c)=>{
93
88
  }, 400);
94
89
  }
95
90
  const body = parseResult.data;
96
- // Validate media for post type
91
+ // Validate media count
97
92
  if (body.mediaIds) {
98
- const mediaError = validateMediaForPostType(body.type, body.mediaIds);
93
+ const mediaError = validateMediaCount(body.mediaIds);
99
94
  if (mediaError) {
100
95
  return c.json({
101
96
  error: mediaError
@@ -112,13 +107,17 @@ postsApiRoutes.post("/", requireAuthApi(), async (c)=>{
112
107
  }
113
108
  }
114
109
  const post = await c.var.services.posts.create({
115
- type: body.type,
110
+ format: body.format,
116
111
  title: body.title,
117
- content: body.content,
118
- visibility: body.visibility,
119
- sourceUrl: body.sourceUrl || undefined,
120
- sourceName: body.sourceName,
121
- path: body.path || undefined,
112
+ body: body.body,
113
+ slug: body.slug || undefined,
114
+ status: body.status,
115
+ featured: body.featured,
116
+ pinned: body.pinned,
117
+ url: body.url || undefined,
118
+ quoteText: body.quoteText,
119
+ rating: body.rating || undefined,
120
+ collectionId: body.collectionId || undefined,
122
121
  replyToId: body.replyToId ? sqid.decode(body.replyToId) ?? undefined : undefined,
123
122
  publishedAt: body.publishedAt
124
123
  });
@@ -152,18 +151,9 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
152
151
  }, 400);
153
152
  }
154
153
  const body = parseResult.data;
155
- // Validate media for post type if mediaIds is provided
154
+ // Validate media count if mediaIds is provided
156
155
  if (body.mediaIds !== undefined) {
157
- // Need the post type — use the new type if provided, else fetch existing
158
- let postType = body.type;
159
- if (!postType) {
160
- const existing = await c.var.services.posts.getById(id);
161
- if (!existing) return c.json({
162
- error: "Not found"
163
- }, 404);
164
- postType = existing.type;
165
- }
166
- const mediaError = validateMediaForPostType(postType, body.mediaIds);
156
+ const mediaError = validateMediaCount(body.mediaIds);
167
157
  if (mediaError) {
168
158
  return c.json({
169
159
  error: mediaError
@@ -180,13 +170,17 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
180
170
  }
181
171
  }
182
172
  const post = await c.var.services.posts.update(id, {
183
- type: body.type,
173
+ format: body.format,
184
174
  title: body.title,
185
- content: body.content,
186
- visibility: body.visibility,
187
- sourceUrl: body.sourceUrl,
188
- sourceName: body.sourceName,
189
- path: body.path,
175
+ body: body.body,
176
+ slug: body.slug,
177
+ status: body.status,
178
+ featured: body.featured,
179
+ pinned: body.pinned,
180
+ url: body.url,
181
+ quoteText: body.quoteText,
182
+ rating: body.rating || undefined,
183
+ collectionId: body.collectionId || undefined,
190
184
  publishedAt: body.publishedAt
191
185
  });
192
186
  if (!post) return c.json({
@@ -21,21 +21,20 @@ searchApiRoutes.get("/", async (c)=>{
21
21
  try {
22
22
  const results = await c.var.services.search.search(query, {
23
23
  limit,
24
- visibility: [
25
- "featured",
26
- "quiet"
24
+ status: [
25
+ "published"
27
26
  ]
28
27
  });
29
28
  return c.json({
30
29
  query,
31
30
  results: results.map((r)=>({
32
31
  id: sqid.encode(r.post.id),
33
- type: r.post.type,
32
+ format: r.post.format,
34
33
  title: r.post.title,
35
- path: r.post.path,
34
+ slug: r.post.slug,
36
35
  snippet: r.snippet,
37
36
  publishedAt: r.post.publishedAt,
38
- url: `/p/${sqid.encode(r.post.id)}`
37
+ url: r.post.slug ? `/${r.post.slug}` : `/p/${sqid.encode(r.post.id)}`
39
38
  })),
40
39
  count: results.length
41
40
  });
@@ -8,7 +8,7 @@ import { html } from "hono/html";
8
8
  import { uuidv7 } from "uuidv7";
9
9
  import { requireAuthApi } from "../../middleware/auth.js";
10
10
  import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "../../lib/image.js";
11
- import { sse, dsSignals } from "../../lib/sse.js";
11
+ import { sse } from "../../lib/sse.js";
12
12
  export const uploadApiRoutes = new Hono();
13
13
  // Require auth for all upload routes
14
14
  uploadApiRoutes.use("*", requireAuthApi());
@@ -85,14 +85,20 @@ function formatSize(bytes) {
85
85
  const accept = c.req.header("accept") || "";
86
86
  return accept.includes("text/event-stream");
87
87
  }
88
+ /**
89
+ * Return an SSE error response that removes the upload placeholder and shows a toast
90
+ */ function sseUploadError(c, message) {
91
+ return sse(c, async (stream)=>{
92
+ await stream.remove("#upload-placeholder");
93
+ await stream.toast(message, "error");
94
+ });
95
+ }
88
96
  // Upload a file
89
97
  uploadApiRoutes.post("/", async (c)=>{
90
98
  const storage = c.var.storage;
91
99
  if (!storage) {
92
100
  if (wantsSSE(c)) {
93
- return dsSignals({
94
- _uploadError: "Storage not configured"
95
- });
101
+ return sseUploadError(c, "Storage not configured");
96
102
  }
97
103
  return c.json({
98
104
  error: "Storage not configured"
@@ -102,9 +108,7 @@ uploadApiRoutes.post("/", async (c)=>{
102
108
  const file = formData.get("file");
103
109
  if (!file) {
104
110
  if (wantsSSE(c)) {
105
- return dsSignals({
106
- _uploadError: "No file provided"
107
- });
111
+ return sseUploadError(c, "No file provided");
108
112
  }
109
113
  return c.json({
110
114
  error: "No file provided"
@@ -120,9 +124,7 @@ uploadApiRoutes.post("/", async (c)=>{
120
124
  ];
121
125
  if (!allowedTypes.includes(file.type)) {
122
126
  if (wantsSSE(c)) {
123
- return dsSignals({
124
- _uploadError: "File type not allowed"
125
- });
127
+ return sseUploadError(c, "File type not allowed");
126
128
  }
127
129
  return c.json({
128
130
  error: "File type not allowed"
@@ -132,9 +134,7 @@ uploadApiRoutes.post("/", async (c)=>{
132
134
  const maxSize = 10 * 1024 * 1024;
133
135
  if (file.size > maxSize) {
134
136
  if (wantsSSE(c)) {
135
- return dsSignals({
136
- _uploadError: "File too large (max 10MB)"
137
- });
137
+ return sseUploadError(c, "File too large (max 10MB)");
138
138
  }
139
139
  return c.json({
140
140
  error: "File too large (max 10MB)"
@@ -43,7 +43,7 @@ function CollectionsListContent({ collections }) {
43
43
  id: "ePK91l",
44
44
  message: "Edit"
45
45
  }),
46
- viewHref: `/c/${col.path}`,
46
+ viewHref: `/c/${col.slug}`,
47
47
  viewLabel: $__i18n._({
48
48
  id: "jpctdh",
49
49
  message: "View"
@@ -59,7 +59,7 @@ function CollectionsListContent({ collections }) {
59
59
  class: "text-sm text-muted-foreground",
60
60
  children: [
61
61
  "/",
62
- col.path
62
+ col.slug
63
63
  ]
64
64
  }),
65
65
  col.description && /*#__PURE__*/ _jsx("p", {
@@ -84,7 +84,7 @@ function NewCollectionContent() {
84
84
  })
85
85
  }),
86
86
  /*#__PURE__*/ _jsxs("form", {
87
- "data-signals": "{title: '', path: '', description: ''}",
87
+ "data-signals": "{title: '', slug: '', description: ''}",
88
88
  "data-on:submit__prevent": "@post('/dash/collections')",
89
89
  "data-indicator": "_loading",
90
90
  class: "flex flex-col gap-4 max-w-lg",
@@ -123,7 +123,7 @@ function NewCollectionContent() {
123
123
  }),
124
124
  /*#__PURE__*/ _jsx("input", {
125
125
  type: "text",
126
- "data-bind": "path",
126
+ "data-bind": "slug",
127
127
  class: "input",
128
128
  required: true,
129
129
  placeholder: "my-collection",
@@ -219,7 +219,7 @@ function ViewCollectionContent({ collection, posts }) {
219
219
  class: "text-sm text-muted-foreground",
220
220
  children: [
221
221
  "/",
222
- collection.path
222
+ collection.slug
223
223
  ]
224
224
  })
225
225
  ]
@@ -230,7 +230,7 @@ function ViewCollectionContent({ collection, posts }) {
230
230
  id: "ePK91l",
231
231
  message: "Edit"
232
232
  }),
233
- viewHref: `/c/${collection.path}`,
233
+ viewHref: `/c/${collection.slug}`,
234
234
  viewLabel: $__i18n._({
235
235
  id: "jpctdh",
236
236
  message: "View"
@@ -259,27 +259,16 @@ function ViewCollectionContent({ collection, posts }) {
259
259
  })
260
260
  }) : /*#__PURE__*/ _jsx("div", {
261
261
  class: "flex flex-col divide-y",
262
- children: posts.map((post)=>/*#__PURE__*/ _jsxs("div", {
262
+ children: posts.map((post)=>/*#__PURE__*/ _jsx("div", {
263
263
  class: "py-3 flex items-center gap-4",
264
- children: [
265
- /*#__PURE__*/ _jsx("div", {
266
- class: "flex-1 min-w-0",
267
- children: /*#__PURE__*/ _jsx("a", {
268
- href: `/dash/posts/${sqid.encode(post.id)}`,
269
- class: "font-medium hover:underline",
270
- children: post.title || post.content?.slice(0, 50) || `Post #${post.id}`
271
- })
272
- }),
273
- /*#__PURE__*/ _jsx("button", {
274
- type: "button",
275
- class: "btn-sm-ghost text-destructive",
276
- "data-on:click__prevent": `@post('/dash/collections/${collection.id}/remove-post', {payload: {postId: ${post.id}}})`,
277
- children: $__i18n._({
278
- id: "t/YqKh",
279
- message: "Remove"
280
- })
264
+ children: /*#__PURE__*/ _jsx("div", {
265
+ class: "flex-1 min-w-0",
266
+ children: /*#__PURE__*/ _jsx("a", {
267
+ href: `/dash/posts/${sqid.encode(post.id)}`,
268
+ class: "font-medium hover:underline",
269
+ children: post.title || post.body?.slice(0, 50) || `Post #${post.id}`
281
270
  })
282
- ]
271
+ })
283
272
  }, post.id))
284
273
  })
285
274
  })
@@ -303,7 +292,7 @@ function EditCollectionContent({ collection }) {
303
292
  const { i18n: $__i18n, _: $__ } = $_useLingui();
304
293
  const signals = JSON.stringify({
305
294
  title: collection.title,
306
- path: collection.path ?? "",
295
+ slug: collection.slug ?? "",
307
296
  description: collection.description ?? ""
308
297
  }).replace(/</g, "\\u003c");
309
298
  return /*#__PURE__*/ _jsxs(_Fragment, {
@@ -351,7 +340,7 @@ function EditCollectionContent({ collection }) {
351
340
  }),
352
341
  /*#__PURE__*/ _jsx("input", {
353
342
  type: "text",
354
- "data-bind": "path",
343
+ "data-bind": "slug",
355
344
  class: "input",
356
345
  required: true,
357
346
  pattern: "[a-z0-9-]+"
@@ -453,7 +442,7 @@ collectionsRoutes.post("/", async (c)=>{
453
442
  const body = await c.req.json();
454
443
  const collection = await c.var.services.collections.create({
455
444
  title: body.title,
456
- path: body.path,
445
+ slug: body.slug,
457
446
  description: body.description || undefined
458
447
  });
459
448
  return dsRedirect(`/dash/collections/${collection.id}`);
@@ -464,7 +453,10 @@ collectionsRoutes.get("/:id", async (c)=>{
464
453
  if (isNaN(id)) return c.notFound();
465
454
  const collection = await c.var.services.collections.getById(id);
466
455
  if (!collection) return c.notFound();
467
- const posts = await c.var.services.collections.getPosts(id);
456
+ // Fetch posts in this collection via post service
457
+ const posts = await c.var.services.posts.list({
458
+ collectionId: id
459
+ });
468
460
  const siteName = await getSiteName(c);
469
461
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
470
462
  c: c,
@@ -501,7 +493,7 @@ collectionsRoutes.post("/:id", async (c)=>{
501
493
  const body = await c.req.json();
502
494
  await c.var.services.collections.update(id, {
503
495
  title: body.title,
504
- path: body.path,
496
+ slug: body.slug,
505
497
  description: body.description || undefined
506
498
  });
507
499
  return dsRedirect(`/dash/collections/${id}`);
@@ -513,13 +505,3 @@ collectionsRoutes.post("/:id/delete", async (c)=>{
513
505
  await c.var.services.collections.delete(id);
514
506
  return dsRedirect("/dash/collections");
515
507
  });
516
- // Remove post from collection
517
- collectionsRoutes.post("/:id/remove-post", async (c)=>{
518
- const id = parseInt(c.req.param("id"), 10);
519
- if (isNaN(id)) return c.notFound();
520
- const body = await c.req.json();
521
- if (body.postId) {
522
- await c.var.services.collections.removePost(id, body.postId);
523
- }
524
- return dsRedirect(`/dash/collections/${id}`);
525
- });
@@ -103,8 +103,8 @@ dashIndexRoutes.get("/", async (c)=>{
103
103
  const allPosts = await c.var.services.posts.list({
104
104
  limit: 1000
105
105
  });
106
- const publishedPosts = allPosts.filter((p)=>p.visibility !== "draft");
107
- const draftPosts = allPosts.filter((p)=>p.visibility === "draft");
106
+ const publishedPosts = allPosts.filter((p)=>p.status !== "draft");
107
+ const draftPosts = allPosts.filter((p)=>p.status === "draft");
108
108
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
109
109
  c: c,
110
110
  title: "Dashboard",
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
2
2
  import { getSiteName } from "../../lib/config.js";
3
3
  /**
4
- * Dashboard Navigation Links Routes
4
+ * Dashboard Navigation Items Routes
5
5
  */ import { Hono } from "hono";
6
6
  import { useLingui as $_useLingui } from "@jant/core/i18n";
7
7
  import { DashLayout } from "../../theme/layouts/index.js";
8
8
  import { EmptyState, ListItemRow, ActionButtons, CrudPageHeader } from "../../theme/components/index.js";
9
9
  import { dsRedirect, dsToast } from "../../lib/sse.js";
10
10
  export const navigationRoutes = new Hono();
11
- function NavigationListContent({ links }) {
11
+ function NavigationListContent({ items }) {
12
12
  const { i18n: $__i18n, _: $__ } = $_useLingui();
13
13
  return /*#__PURE__*/ _jsxs(_Fragment, {
14
14
  children: [
@@ -23,7 +23,7 @@ function NavigationListContent({ links }) {
23
23
  }),
24
24
  ctaHref: "/dash/navigation/new"
25
25
  }),
26
- links.length === 0 ? /*#__PURE__*/ _jsx(EmptyState, {
26
+ items.length === 0 ? /*#__PURE__*/ _jsx(EmptyState, {
27
27
  message: $__i18n._({
28
28
  id: "wdGjkd",
29
29
  message: "No navigation links configured."
@@ -37,14 +37,14 @@ function NavigationListContent({ links }) {
37
37
  children: /*#__PURE__*/ _jsx("div", {
38
38
  id: "nav-links-list",
39
39
  class: "flex flex-col divide-y",
40
- children: links.map((link)=>/*#__PURE__*/ _jsx(ListItemRow, {
40
+ children: items.map((item)=>/*#__PURE__*/ _jsx(ListItemRow, {
41
41
  actions: /*#__PURE__*/ _jsx(ActionButtons, {
42
- editHref: `/dash/navigation/${link.id}/edit`,
42
+ editHref: `/dash/navigation/${item.id}/edit`,
43
43
  editLabel: $__i18n._({
44
44
  id: "ePK91l",
45
45
  message: "Edit"
46
46
  }),
47
- deleteAction: `/dash/navigation/${link.id}/delete`,
47
+ deleteAction: `/dash/navigation/${item.id}/delete`,
48
48
  deleteLabel: $__i18n._({
49
49
  id: "cnGeoo",
50
50
  message: "Delete"
@@ -52,7 +52,7 @@ function NavigationListContent({ links }) {
52
52
  }),
53
53
  children: /*#__PURE__*/ _jsxs("div", {
54
54
  class: "flex items-center gap-3 cursor-grab",
55
- "data-id": link.id,
55
+ "data-id": item.id,
56
56
  children: [
57
57
  /*#__PURE__*/ _jsx("span", {
58
58
  class: "text-muted-foreground select-none",
@@ -63,23 +63,23 @@ function NavigationListContent({ links }) {
63
63
  children: [
64
64
  /*#__PURE__*/ _jsx("span", {
65
65
  class: "font-medium",
66
- children: link.label
66
+ children: item.label
67
67
  }),
68
68
  /*#__PURE__*/ _jsx("code", {
69
69
  class: "text-sm text-muted-foreground bg-muted px-1 rounded",
70
- children: link.url
70
+ children: item.url
71
71
  })
72
72
  ]
73
73
  })
74
74
  ]
75
75
  })
76
- }, link.id))
76
+ }, item.id))
77
77
  })
78
78
  })
79
79
  ]
80
80
  });
81
81
  }
82
- function NavigationFormContent({ link, isEdit }) {
82
+ function NavigationFormContent({ item, isEdit }) {
83
83
  const { i18n: $__i18n, _: $__ } = $_useLingui();
84
84
  const title = isEdit ? $__i18n._({
85
85
  id: "gDx5MG",
@@ -89,10 +89,10 @@ function NavigationFormContent({ link, isEdit }) {
89
89
  message: "New Link"
90
90
  });
91
91
  const signals = JSON.stringify({
92
- label: link?.label ?? "",
93
- url: link?.url ?? ""
92
+ label: item?.label ?? "",
93
+ url: item?.url ?? ""
94
94
  }).replace(/</g, "\\u003c");
95
- const action = isEdit ? `/dash/navigation/${link?.id}` : "/dash/navigation";
95
+ const action = isEdit ? `/dash/navigation/${item?.id}` : "/dash/navigation";
96
96
  return /*#__PURE__*/ _jsxs(_Fragment, {
97
97
  children: [
98
98
  /*#__PURE__*/ _jsx("h1", {
@@ -199,17 +199,17 @@ function NavigationFormContent({ link, isEdit }) {
199
199
  ]
200
200
  });
201
201
  }
202
- // List navigation links
202
+ // List navigation items
203
203
  navigationRoutes.get("/", async (c)=>{
204
204
  const siteName = await getSiteName(c);
205
- const links = await c.var.services.navigationLinks.list();
205
+ const items = await c.var.services.navItems.list();
206
206
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
207
207
  c: c,
208
208
  title: "Navigation",
209
209
  siteName: siteName,
210
210
  currentPath: "/dash/navigation",
211
211
  children: /*#__PURE__*/ _jsx(NavigationListContent, {
212
- links: links
212
+ items: items
213
213
  })
214
214
  }));
215
215
  });
@@ -230,7 +230,8 @@ navigationRoutes.post("/", async (c)=>{
230
230
  if (!body.label || !body.url) {
231
231
  return dsToast("Label and URL are required", "error");
232
232
  }
233
- await c.var.services.navigationLinks.create({
233
+ await c.var.services.navItems.create({
234
+ type: "link",
234
235
  label: body.label,
235
236
  url: body.url
236
237
  });
@@ -242,15 +243,15 @@ navigationRoutes.post("/reorder", async (c)=>{
242
243
  if (!Array.isArray(body.ids)) {
243
244
  return dsToast("Invalid request", "error");
244
245
  }
245
- await c.var.services.navigationLinks.reorder(body.ids);
246
+ await c.var.services.navItems.reorder(body.ids);
246
247
  return dsToast("Order saved");
247
248
  });
248
249
  // Edit link form
249
250
  navigationRoutes.get("/:id/edit", async (c)=>{
250
251
  const id = parseInt(c.req.param("id"), 10);
251
252
  if (isNaN(id)) return c.notFound();
252
- const link = await c.var.services.navigationLinks.getById(id);
253
- if (!link) return c.notFound();
253
+ const item = await c.var.services.navItems.getById(id);
254
+ if (!item) return c.notFound();
254
255
  const siteName = await getSiteName(c);
255
256
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
256
257
  c: c,
@@ -258,7 +259,7 @@ navigationRoutes.get("/:id/edit", async (c)=>{
258
259
  siteName: siteName,
259
260
  currentPath: "/dash/navigation",
260
261
  children: /*#__PURE__*/ _jsx(NavigationFormContent, {
261
- link: link,
262
+ item: item,
262
263
  isEdit: true
263
264
  })
264
265
  }));
@@ -271,7 +272,7 @@ navigationRoutes.post("/:id", async (c)=>{
271
272
  if (!body.label || !body.url) {
272
273
  return dsToast("Label and URL are required", "error");
273
274
  }
274
- const updated = await c.var.services.navigationLinks.update(id, {
275
+ const updated = await c.var.services.navItems.update(id, {
275
276
  label: body.label,
276
277
  url: body.url
277
278
  });
@@ -282,7 +283,7 @@ navigationRoutes.post("/:id", async (c)=>{
282
283
  navigationRoutes.post("/:id/delete", async (c)=>{
283
284
  const id = parseInt(c.req.param("id"), 10);
284
285
  if (!isNaN(id)) {
285
- await c.var.services.navigationLinks.delete(id);
286
+ await c.var.services.navItems.delete(id);
286
287
  }
287
288
  return dsRedirect("/dash/navigation");
288
289
  });