@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.
Files changed (248) hide show
  1. package/dist/app.js +50 -26
  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 -11
  7. package/dist/lib/constants.js +2 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/nav-reorder.js +1 -1
  11. package/dist/lib/navigation.js +30 -6
  12. package/dist/lib/pagination.js +44 -0
  13. package/dist/lib/render.js +7 -11
  14. package/dist/lib/schemas.js +80 -38
  15. package/dist/lib/theme.js +4 -4
  16. package/dist/lib/time.js +56 -1
  17. package/dist/lib/timeline.js +95 -0
  18. package/dist/lib/view.js +61 -72
  19. package/dist/routes/api/collections.js +124 -0
  20. package/dist/routes/api/nav-items.js +104 -0
  21. package/dist/routes/api/pages.js +91 -0
  22. package/dist/routes/api/posts.js +27 -33
  23. package/dist/routes/api/search.js +4 -5
  24. package/dist/routes/api/settings.js +68 -0
  25. package/dist/routes/api/upload.js +13 -13
  26. package/dist/routes/compose.js +48 -0
  27. package/dist/routes/dash/collections.js +24 -42
  28. package/dist/routes/dash/index.js +3 -3
  29. package/dist/routes/dash/media.js +2 -2
  30. package/dist/routes/dash/pages.js +440 -106
  31. package/dist/routes/dash/posts.js +27 -37
  32. package/dist/routes/dash/redirects.js +2 -2
  33. package/dist/routes/dash/settings.js +79 -5
  34. package/dist/routes/feed/rss.js +4 -6
  35. package/dist/routes/feed/sitemap.js +11 -8
  36. package/dist/routes/pages/archive.js +13 -15
  37. package/dist/routes/pages/collection.js +12 -9
  38. package/dist/routes/pages/collections.js +28 -0
  39. package/dist/routes/pages/featured.js +32 -0
  40. package/dist/routes/pages/home.js +19 -68
  41. package/dist/routes/pages/page.js +57 -29
  42. package/dist/routes/pages/post.js +7 -17
  43. package/dist/routes/pages/search.js +5 -9
  44. package/dist/services/collection.js +52 -64
  45. package/dist/services/index.js +5 -3
  46. package/dist/services/navigation.js +29 -53
  47. package/dist/services/page.js +84 -0
  48. package/dist/services/post.js +102 -69
  49. package/dist/services/search.js +24 -18
  50. package/dist/types.js +24 -40
  51. package/dist/ui/compose/ComposeDialog.js +452 -0
  52. package/dist/ui/compose/ComposePrompt.js +55 -0
  53. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
  54. package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
  55. package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
  56. package/dist/{theme/components → ui/dash}/PostList.js +18 -13
  57. package/dist/ui/dash/StatusBadge.js +46 -0
  58. package/dist/{theme/components → ui/dash}/index.js +3 -6
  59. package/dist/ui/feed/LinkCard.js +72 -0
  60. package/dist/ui/feed/NoteCard.js +58 -0
  61. package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
  62. package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
  63. package/dist/ui/feed/TimelineFeed.js +41 -0
  64. package/dist/ui/feed/TimelineItem.js +27 -0
  65. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  66. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  67. package/dist/ui/layouts/SiteLayout.js +141 -0
  68. package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
  69. package/dist/ui/pages/CollectionPage.js +70 -0
  70. package/dist/ui/pages/CollectionsPage.js +76 -0
  71. package/dist/ui/pages/FeaturedPage.js +24 -0
  72. package/dist/ui/pages/HomePage.js +24 -0
  73. package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
  74. package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
  75. package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
  76. package/dist/ui/shared/MediaGallery.js +35 -0
  77. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  78. package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
  79. package/dist/ui/shared/index.js +5 -0
  80. package/package.json +2 -9
  81. package/src/__tests__/helpers/app.ts +4 -0
  82. package/src/__tests__/helpers/db.ts +53 -73
  83. package/src/app.tsx +56 -28
  84. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  85. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  86. package/src/db/migrations/meta/_journal.json +14 -0
  87. package/src/db/schema.ts +63 -46
  88. package/src/i18n/locales/en.po +443 -240
  89. package/src/i18n/locales/en.ts +1 -1
  90. package/src/i18n/locales/zh-Hans.po +443 -240
  91. package/src/i18n/locales/zh-Hans.ts +1 -1
  92. package/src/i18n/locales/zh-Hant.po +443 -240
  93. package/src/i18n/locales/zh-Hant.ts +1 -1
  94. package/src/index.ts +29 -42
  95. package/src/lib/__tests__/excerpt.test.ts +125 -0
  96. package/src/lib/__tests__/schemas.test.ts +201 -99
  97. package/src/lib/__tests__/time.test.ts +62 -0
  98. package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
  99. package/src/lib/__tests__/view.test.ts +204 -50
  100. package/src/lib/constants.ts +2 -4
  101. package/src/lib/excerpt.ts +87 -0
  102. package/src/lib/feed.ts +22 -7
  103. package/src/lib/nav-reorder.ts +1 -1
  104. package/src/lib/navigation.ts +45 -8
  105. package/src/lib/pagination.ts +50 -0
  106. package/src/lib/render.tsx +7 -14
  107. package/src/lib/schemas.ts +119 -51
  108. package/src/lib/theme.ts +5 -5
  109. package/src/lib/time.ts +64 -0
  110. package/src/lib/timeline.ts +141 -0
  111. package/src/lib/view.ts +80 -82
  112. package/src/preset.css +46 -0
  113. package/src/routes/__tests__/compose.test.ts +199 -0
  114. package/src/routes/api/__tests__/collections.test.ts +249 -0
  115. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  116. package/src/routes/api/__tests__/pages.test.ts +218 -0
  117. package/src/routes/api/__tests__/posts.test.ts +50 -108
  118. package/src/routes/api/__tests__/search.test.ts +2 -3
  119. package/src/routes/api/__tests__/settings.test.ts +132 -0
  120. package/src/routes/api/collections.ts +143 -0
  121. package/src/routes/api/nav-items.ts +115 -0
  122. package/src/routes/api/pages.ts +101 -0
  123. package/src/routes/api/posts.ts +28 -28
  124. package/src/routes/api/search.ts +3 -3
  125. package/src/routes/api/settings.ts +91 -0
  126. package/src/routes/api/upload.ts +16 -6
  127. package/src/routes/compose.ts +63 -0
  128. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  129. package/src/routes/dash/collections.tsx +20 -42
  130. package/src/routes/dash/index.tsx +3 -3
  131. package/src/routes/dash/media.tsx +2 -2
  132. package/src/routes/dash/pages.tsx +480 -122
  133. package/src/routes/dash/posts.tsx +42 -54
  134. package/src/routes/dash/redirects.tsx +2 -2
  135. package/src/routes/dash/settings.tsx +83 -5
  136. package/src/routes/feed/rss.ts +4 -3
  137. package/src/routes/feed/sitemap.ts +15 -5
  138. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  139. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  140. package/src/routes/pages/archive.tsx +15 -15
  141. package/src/routes/pages/collection.tsx +16 -9
  142. package/src/routes/pages/collections.tsx +36 -0
  143. package/src/routes/pages/featured.tsx +38 -0
  144. package/src/routes/pages/home.tsx +21 -92
  145. package/src/routes/pages/page.tsx +62 -27
  146. package/src/routes/pages/post.tsx +6 -18
  147. package/src/routes/pages/search.tsx +3 -7
  148. package/src/services/__tests__/collection.test.ts +257 -158
  149. package/src/services/__tests__/media.test.ts +18 -18
  150. package/src/services/__tests__/navigation.test.ts +161 -87
  151. package/src/services/__tests__/page.test.ts +106 -0
  152. package/src/services/__tests__/post-timeline.test.ts +92 -88
  153. package/src/services/__tests__/post.test.ts +432 -197
  154. package/src/services/__tests__/search.test.ts +19 -25
  155. package/src/services/collection.ts +71 -113
  156. package/src/services/index.ts +9 -8
  157. package/src/services/navigation.ts +38 -71
  158. package/src/services/page.ts +136 -0
  159. package/src/services/post.ts +141 -101
  160. package/src/services/search.ts +38 -27
  161. package/src/styles/tokens.css +47 -0
  162. package/src/styles/ui.css +491 -0
  163. package/src/types.ts +212 -198
  164. package/src/ui/compose/ComposeDialog.tsx +395 -0
  165. package/src/ui/compose/ComposePrompt.tsx +55 -0
  166. package/src/ui/dash/FormatBadge.tsx +28 -0
  167. package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
  168. package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
  169. package/src/ui/dash/PostList.tsx +101 -0
  170. package/src/ui/dash/StatusBadge.tsx +61 -0
  171. package/src/ui/dash/index.ts +10 -0
  172. package/src/ui/feed/LinkCard.tsx +72 -0
  173. package/src/ui/feed/NoteCard.tsx +63 -0
  174. package/src/ui/feed/QuoteCard.tsx +68 -0
  175. package/src/ui/feed/ThreadPreview.tsx +48 -0
  176. package/src/ui/feed/TimelineFeed.tsx +49 -0
  177. package/src/ui/feed/TimelineItem.tsx +45 -0
  178. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  179. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  180. package/src/ui/layouts/SiteLayout.tsx +150 -0
  181. package/src/ui/pages/ArchivePage.tsx +162 -0
  182. package/src/ui/pages/CollectionPage.tsx +70 -0
  183. package/src/ui/pages/CollectionsPage.tsx +73 -0
  184. package/src/ui/pages/FeaturedPage.tsx +31 -0
  185. package/src/ui/pages/HomePage.tsx +37 -0
  186. package/src/ui/pages/PostPage.tsx +56 -0
  187. package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
  188. package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
  189. package/src/ui/shared/MediaGallery.tsx +59 -0
  190. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  191. package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
  192. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  193. package/src/ui/shared/index.ts +12 -0
  194. package/bin/jant.js +0 -185
  195. package/dist/lib/theme-components.js +0 -49
  196. package/dist/routes/api/timeline.js +0 -120
  197. package/dist/routes/dash/navigation.js +0 -288
  198. package/dist/theme/components/MediaGallery.js +0 -107
  199. package/dist/theme/components/VisibilityBadge.js +0 -37
  200. package/dist/theme/index.js +0 -18
  201. package/dist/theme/layouts/index.js +0 -2
  202. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  203. package/dist/themes/minimal/index.js +0 -65
  204. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  205. package/dist/themes/minimal/pages/HomePage.js +0 -25
  206. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  207. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  208. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  209. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  210. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  211. package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
  212. package/src/lib/__tests__/theme-components.test.ts +0 -126
  213. package/src/lib/theme-components.ts +0 -68
  214. package/src/routes/api/timeline.tsx +0 -159
  215. package/src/routes/dash/navigation.tsx +0 -316
  216. package/src/theme/components/MediaGallery.tsx +0 -128
  217. package/src/theme/components/PostList.tsx +0 -92
  218. package/src/theme/components/TypeBadge.tsx +0 -37
  219. package/src/theme/components/VisibilityBadge.tsx +0 -45
  220. package/src/theme/components/index.ts +0 -23
  221. package/src/theme/index.ts +0 -22
  222. package/src/theme/layouts/index.ts +0 -7
  223. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  224. package/src/themes/minimal/index.ts +0 -83
  225. package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
  226. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  227. package/src/themes/minimal/pages/HomePage.tsx +0 -41
  228. package/src/themes/minimal/pages/PostPage.tsx +0 -43
  229. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  230. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  231. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  232. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  233. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  234. package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
  235. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
  236. package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
  237. /package/dist/{theme → ui}/color-themes.js +0 -0
  238. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  239. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  240. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  241. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  242. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  243. /package/src/{theme → ui}/color-themes.ts +0 -0
  244. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  245. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  246. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  247. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  248. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -1,26 +1,29 @@
1
1
  /**
2
- * Post Service
2
+ * Post Service (v2)
3
3
  *
4
- * CRUD operations for posts with Thread support
5
- */ import { eq, and, isNull, desc, or, inArray, notInArray, sql } from "drizzle-orm";
4
+ * CRUD operations for posts with Thread support.
5
+ * Posts have format (note/link/quote), status (draft/published),
6
+ * featured flag, and pinned flag.
7
+ */ import { eq, and, isNull, desc, or, inArray, sql } from "drizzle-orm";
6
8
  import { posts } from "../db/schema.js";
7
9
  import { now } from "../lib/time.js";
8
- import { extractDomain } from "../lib/url.js";
9
10
  import { render as renderMarkdown } from "../lib/markdown.js";
10
11
  export function createPostService(db) {
11
- // Helper to map DB row to Post type
12
12
  function toPost(row) {
13
13
  return {
14
14
  id: row.id,
15
- type: row.type,
16
- visibility: row.visibility,
17
- title: row.title,
15
+ format: row.format,
16
+ status: row.status,
17
+ featured: row.featured,
18
+ pinned: row.pinned,
18
19
  path: row.path,
19
- content: row.content,
20
- contentHtml: row.contentHtml,
21
- sourceUrl: row.sourceUrl,
22
- sourceName: row.sourceName,
23
- sourceDomain: row.sourceDomain,
20
+ title: row.title,
21
+ url: row.url,
22
+ body: row.body,
23
+ bodyHtml: row.bodyHtml,
24
+ quoteText: row.quoteText,
25
+ rating: row.rating,
26
+ collectionId: row.collectionId,
24
27
  replyToId: row.replyToId,
25
28
  threadId: row.threadId,
26
29
  deletedAt: row.deletedAt,
@@ -40,73 +43,103 @@ export function createPostService(db) {
40
43
  },
41
44
  async list (filters = {}) {
42
45
  const conditions = [];
43
- // Visibility filter
44
- if (filters.visibility) {
45
- if (Array.isArray(filters.visibility)) {
46
- conditions.push(inArray(posts.visibility, filters.visibility));
47
- } else {
48
- conditions.push(eq(posts.visibility, filters.visibility));
49
- }
46
+ if (filters.status) {
47
+ conditions.push(eq(posts.status, filters.status));
50
48
  }
51
- // Type filter
52
- if (filters.type) {
53
- conditions.push(eq(posts.type, filters.type));
49
+ if (filters.featured !== undefined) {
50
+ conditions.push(eq(posts.featured, filters.featured ? 1 : 0));
54
51
  }
55
- // Exclude types filter
56
- if (filters.excludeTypes && filters.excludeTypes.length > 0) {
57
- conditions.push(notInArray(posts.type, filters.excludeTypes));
52
+ if (filters.pinned !== undefined) {
53
+ conditions.push(eq(posts.pinned, filters.pinned ? 1 : 0));
54
+ }
55
+ if (filters.format) {
56
+ conditions.push(eq(posts.format, filters.format));
57
+ }
58
+ if (filters.collectionId !== undefined) {
59
+ conditions.push(eq(posts.collectionId, filters.collectionId));
58
60
  }
59
- // Thread filter
60
61
  if (filters.threadId) {
61
62
  conditions.push(eq(posts.threadId, filters.threadId));
62
63
  }
63
- // Exclude replies (posts that are part of a thread but not the root)
64
64
  if (filters.excludeReplies) {
65
65
  conditions.push(isNull(posts.threadId));
66
66
  }
67
- // Exclude deleted unless specified
68
67
  if (!filters.includeDeleted) {
69
68
  conditions.push(isNull(posts.deletedAt));
70
69
  }
71
- // Cursor pagination
72
70
  if (filters.cursor) {
73
71
  conditions.push(sql`${posts.id} < ${filters.cursor}`);
74
72
  }
75
- const query = db.select().from(posts).where(conditions.length > 0 ? and(...conditions) : undefined).orderBy(desc(posts.publishedAt), desc(posts.id)).limit(filters.limit ?? 100);
73
+ let query = db.select().from(posts).where(conditions.length > 0 ? and(...conditions) : undefined).orderBy(desc(posts.publishedAt), desc(posts.id)).limit(filters.limit ?? 100);
74
+ if (filters.offset !== undefined) {
75
+ query = query.offset(filters.offset);
76
+ }
76
77
  const rows = await query;
77
78
  return rows.map(toPost);
78
79
  },
80
+ async count (filters = {}) {
81
+ const conditions = [];
82
+ if (filters.status) {
83
+ conditions.push(eq(posts.status, filters.status));
84
+ }
85
+ if (filters.featured !== undefined) {
86
+ conditions.push(eq(posts.featured, filters.featured ? 1 : 0));
87
+ }
88
+ if (filters.pinned !== undefined) {
89
+ conditions.push(eq(posts.pinned, filters.pinned ? 1 : 0));
90
+ }
91
+ if (filters.format) {
92
+ conditions.push(eq(posts.format, filters.format));
93
+ }
94
+ if (filters.collectionId !== undefined) {
95
+ conditions.push(eq(posts.collectionId, filters.collectionId));
96
+ }
97
+ if (filters.threadId) {
98
+ conditions.push(eq(posts.threadId, filters.threadId));
99
+ }
100
+ if (filters.excludeReplies) {
101
+ conditions.push(isNull(posts.threadId));
102
+ }
103
+ if (!filters.includeDeleted) {
104
+ conditions.push(isNull(posts.deletedAt));
105
+ }
106
+ const result = await db.select({
107
+ count: sql`count(*)`.as("count")
108
+ }).from(posts).where(conditions.length > 0 ? and(...conditions) : undefined);
109
+ return result[0]?.count ?? 0;
110
+ },
79
111
  async create (data) {
80
112
  const timestamp = now();
81
- // Process content
82
- const contentHtml = data.content ? renderMarkdown(data.content) : null;
83
- // Extract domain from source URL
84
- const sourceDomain = data.sourceUrl ? extractDomain(data.sourceUrl) : null;
113
+ const bodyHtml = data.body ? renderMarkdown(data.body) : null;
85
114
  // Handle thread relationship
86
115
  let threadId = null;
87
- let visibility = data.visibility ?? "quiet";
116
+ let status = data.status ?? "published";
117
+ let featured = data.featured ?? false;
88
118
  if (data.replyToId) {
89
119
  const parent = await this.getById(data.replyToId);
90
120
  if (parent) {
91
- // thread_id = parent's thread_id or parent's id (if parent is root)
92
121
  threadId = parent.threadId ?? parent.id;
93
- // Inherit visibility from root
122
+ // Inherit status and featured from root
94
123
  const root = parent.threadId ? await this.getById(parent.threadId) : parent;
95
124
  if (root) {
96
- visibility = root.visibility;
125
+ status = root.status;
126
+ featured = root.featured === 1;
97
127
  }
98
128
  }
99
129
  }
100
130
  const result = await db.insert(posts).values({
101
- type: data.type,
102
- visibility,
103
- title: data.title ?? null,
131
+ format: data.format,
132
+ status,
133
+ featured: featured ? 1 : 0,
134
+ pinned: data.pinned ? 1 : 0,
104
135
  path: data.path ?? null,
105
- content: data.content ?? null,
106
- contentHtml,
107
- sourceUrl: data.sourceUrl ?? null,
108
- sourceName: data.sourceName ?? null,
109
- sourceDomain,
136
+ title: data.title ?? null,
137
+ url: data.url ?? null,
138
+ body: data.body ?? null,
139
+ bodyHtml,
140
+ quoteText: data.quoteText ?? null,
141
+ rating: data.rating ?? null,
142
+ collectionId: data.collectionId ?? null,
110
143
  replyToId: data.replyToId ?? null,
111
144
  threadId,
112
145
  publishedAt: data.publishedAt ?? timestamp,
@@ -123,26 +156,27 @@ export function createPostService(db) {
123
156
  const updates = {
124
157
  updatedAt: timestamp
125
158
  };
126
- if (data.type !== undefined) updates.type = data.type;
127
- if (data.title !== undefined) updates.title = data.title;
159
+ if (data.format !== undefined) updates.format = data.format;
128
160
  if (data.path !== undefined) updates.path = data.path;
161
+ if (data.title !== undefined) updates.title = data.title;
162
+ if (data.url !== undefined) updates.url = data.url;
163
+ if (data.quoteText !== undefined) updates.quoteText = data.quoteText;
164
+ if (data.rating !== undefined) updates.rating = data.rating;
165
+ if (data.collectionId !== undefined) updates.collectionId = data.collectionId;
129
166
  if (data.publishedAt !== undefined) updates.publishedAt = data.publishedAt;
130
- if (data.sourceUrl !== undefined) {
131
- updates.sourceUrl = data.sourceUrl;
132
- updates.sourceDomain = data.sourceUrl ? extractDomain(data.sourceUrl) : null;
133
- }
134
- if (data.sourceName !== undefined) updates.sourceName = data.sourceName;
135
- if (data.content !== undefined) {
136
- updates.content = data.content;
137
- updates.contentHtml = data.content ? renderMarkdown(data.content) : null;
138
- }
139
- // Handle visibility change - cascade to thread if this is root
140
- if (data.visibility !== undefined && data.visibility !== existing.visibility) {
141
- updates.visibility = data.visibility;
142
- // If this is a root post, cascade visibility to all thread posts
143
- if (!existing.threadId) {
144
- await this.updateThreadVisibility(id, data.visibility);
145
- }
167
+ if (data.pinned !== undefined) updates.pinned = data.pinned ? 1 : 0;
168
+ if (data.body !== undefined) {
169
+ updates.body = data.body;
170
+ updates.bodyHtml = data.body ? renderMarkdown(data.body) : null;
171
+ }
172
+ // Handle status/featured change - cascade to thread if this is root
173
+ const statusChanged = data.status !== undefined && data.status !== existing.status;
174
+ const featuredChanged = data.featured !== undefined && (data.featured ? 1 : 0) !== existing.featured;
175
+ if (statusChanged) updates.status = data.status;
176
+ if (featuredChanged) updates.featured = data.featured ? 1 : 0;
177
+ // If this is a root post and status/featured changed, cascade to thread
178
+ if ((statusChanged || featuredChanged) && !existing.threadId) {
179
+ await this.updateThreadStatusAndFeatured(id, data.status ?? existing.status, data.featured !== undefined ? data.featured : existing.featured === 1);
146
180
  }
147
181
  const result = await db.update(posts).set(updates).where(eq(posts.id, id)).returning();
148
182
  return result[0] ? toPost(result[0]) : null;
@@ -158,7 +192,6 @@ export function createPostService(db) {
158
192
  updatedAt: timestamp
159
193
  }).where(or(eq(posts.id, id), eq(posts.threadId, id)));
160
194
  } else {
161
- // Just delete this single post
162
195
  await db.update(posts).set({
163
196
  deletedAt: timestamp,
164
197
  updatedAt: timestamp
@@ -170,10 +203,11 @@ export function createPostService(db) {
170
203
  const rows = await db.select().from(posts).where(and(or(eq(posts.id, rootId), eq(posts.threadId, rootId)), isNull(posts.deletedAt))).orderBy(posts.createdAt);
171
204
  return rows.map(toPost);
172
205
  },
173
- async updateThreadVisibility (rootId, visibility) {
206
+ async updateThreadStatusAndFeatured (rootId, status, featured) {
174
207
  const timestamp = now();
175
208
  await db.update(posts).set({
176
- visibility,
209
+ status,
210
+ featured: featured ? 1 : 0,
177
211
  updatedAt: timestamp
178
212
  }).where(eq(posts.threadId, rootId));
179
213
  },
@@ -194,7 +228,6 @@ export function createPostService(db) {
194
228
  async getThreadPreviews (rootIds, previewCount = 3) {
195
229
  if (rootIds.length === 0) return new Map();
196
230
  const rows = await db.select().from(posts).where(and(inArray(posts.threadId, rootIds), isNull(posts.deletedAt))).orderBy(posts.threadId, posts.createdAt);
197
- // Partition by threadId, take first previewCount per thread
198
231
  const result = new Map();
199
232
  for (const row of rows){
200
233
  const post = toPost(row);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Search Service
2
+ * Search Service (v2)
3
3
  *
4
4
  * Full-text search using FTS5
5
5
  */ export function createSearchService(d1) {
@@ -7,19 +7,21 @@
7
7
  async search (query, options = {}) {
8
8
  const limit = options.limit ?? 20;
9
9
  const offset = options.offset ?? 0;
10
- const visibility = options.visibility ?? [
11
- "featured",
12
- "quiet"
10
+ const status = options.status ?? [
11
+ "published"
13
12
  ];
14
13
  // Escape and prepare the query for FTS5
15
- // FTS5 uses * for prefix matching
16
14
  const ftsQuery = query.trim().split(/\s+/).filter((term)=>term.length > 0).map((term)=>`"${term.replace(/"/g, '""')}"*`).join(" ");
17
15
  if (!ftsQuery) {
18
16
  return [];
19
17
  }
20
- // Build visibility placeholders
21
- const visibilityPlaceholders = visibility.map(()=>"?").join(", ");
22
- // Query FTS5 table and join with posts using raw D1 query
18
+ // Build status placeholders
19
+ const statusPlaceholders = status.map(()=>"?").join(", ");
20
+ // Build format filter
21
+ const formatFilter = options.format ? "AND posts.format = ?" : "";
22
+ const formatParams = options.format ? [
23
+ options.format
24
+ ] : [];
23
25
  const stmt = d1.prepare(`
24
26
  SELECT
25
27
  posts.*,
@@ -29,23 +31,27 @@
29
31
  JOIN posts ON posts.id = posts_fts.rowid
30
32
  WHERE posts_fts MATCH ?
31
33
  AND posts.deleted_at IS NULL
32
- AND posts.visibility IN (${visibilityPlaceholders})
34
+ AND posts.status IN (${statusPlaceholders})
35
+ ${formatFilter}
33
36
  ORDER BY posts_fts.rank
34
37
  LIMIT ? OFFSET ?
35
38
  `);
36
- const { results } = await stmt.bind(ftsQuery, ...visibility, limit, offset).all();
39
+ const { results } = await stmt.bind(ftsQuery, ...status, ...formatParams, limit, offset).all();
37
40
  return (results || []).map((row)=>({
38
41
  post: {
39
42
  id: row.id,
40
- type: row.type,
41
- visibility: row.visibility,
42
- title: row.title,
43
+ format: row.format,
44
+ status: row.status,
45
+ featured: row.featured,
46
+ pinned: row.pinned,
43
47
  path: row.path,
44
- content: row.content,
45
- contentHtml: row.content_html,
46
- sourceUrl: row.source_url,
47
- sourceName: row.source_name,
48
- sourceDomain: row.source_domain,
48
+ title: row.title,
49
+ url: row.url,
50
+ body: row.body,
51
+ bodyHtml: row.body_html,
52
+ quoteText: row.quote_text,
53
+ rating: row.rating,
54
+ collectionId: row.collection_id,
49
55
  replyToId: row.reply_to_id,
50
56
  threadId: row.thread_id,
51
57
  deletedAt: row.deleted_at,
package/dist/types.js CHANGED
@@ -1,53 +1,33 @@
1
1
  /**
2
- * Jant Type Definitions
2
+ * Jant Type Definitions (v2)
3
3
  */ // =============================================================================
4
4
  // Content Types
5
5
  // =============================================================================
6
- export const POST_TYPES = [
6
+ export const FORMATS = [
7
7
  "note",
8
- "article",
9
8
  "link",
10
- "quote",
11
- "image",
12
- "page"
9
+ "quote"
10
+ ];
11
+ export const STATUSES = [
12
+ "draft",
13
+ "published"
14
+ ];
15
+ export const SORT_ORDERS = [
16
+ "newest",
17
+ "oldest",
18
+ "rating_desc",
19
+ "rating_asc"
20
+ ];
21
+ export const NAV_ITEM_TYPES = [
22
+ "page",
23
+ "link"
13
24
  ];
14
25
  export const MAX_MEDIA_ATTACHMENTS = 20;
15
- /**
16
- * Media attachment rules per post type.
17
- * Each entry is [min, max] or null (no media allowed).
18
- */ export const POST_TYPE_MEDIA_RULES = {
19
- note: [
20
- 0,
21
- 20
22
- ],
23
- article: [
24
- 0,
25
- 20
26
- ],
27
- image: [
28
- 1,
29
- 20
30
- ],
31
- link: [
32
- 0,
33
- 1
34
- ],
35
- quote: [
36
- 0,
37
- 20
38
- ],
39
- page: null
40
- };
26
+ export const MAX_PINNED_POSTS = 3;
41
27
  export const STORAGE_DRIVERS = [
42
28
  "r2",
43
29
  "s3"
44
30
  ];
45
- export const VISIBILITY_LEVELS = [
46
- "featured",
47
- "quiet",
48
- "unlisted",
49
- "draft"
50
- ];
51
31
  // =============================================================================
52
32
  // Configuration System
53
33
  // =============================================================================
@@ -58,8 +38,8 @@ export const VISIBILITY_LEVELS = [
58
38
  * Add new fields here, and they'll automatically work everywhere.
59
39
  *
60
40
  * Priority logic:
61
- * - envOnly: false User-configurable (DB > ENV > Default)
62
- * - envOnly: true Environment-only (ENV > Default)
41
+ * - envOnly: false -> User-configurable (DB > ENV > Default)
42
+ * - envOnly: true -> Environment-only (ENV > Default)
63
43
  */ export const CONFIG_FIELDS = {
64
44
  // User-configurable (can be modified in dashboard)
65
45
  SITE_NAME: {
@@ -99,6 +79,10 @@ export const VISIBILITY_LEVELS = [
99
79
  defaultValue: "",
100
80
  envOnly: true
101
81
  },
82
+ PAGE_SIZE: {
83
+ defaultValue: "20",
84
+ envOnly: true
85
+ },
102
86
  STORAGE_DRIVER: {
103
87
  defaultValue: "r2",
104
88
  envOnly: true