@jant/core 0.3.27 → 0.3.28

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 (313) hide show
  1. package/dist/client/client.css +1 -0
  2. package/dist/client/client.js +31561 -0
  3. package/dist/index.js +15209 -15
  4. package/package.json +21 -15
  5. package/src/__tests__/helpers/app.ts +19 -3
  6. package/src/__tests__/helpers/db.ts +44 -0
  7. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  8. package/src/app.tsx +111 -174
  9. package/src/client.ts +13 -0
  10. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  11. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  12. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  13. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  14. package/src/db/schema.ts +24 -4
  15. package/src/i18n/locales/en.po +810 -385
  16. package/src/i18n/locales/en.ts +1 -1
  17. package/src/i18n/locales/zh-Hans.po +733 -522
  18. package/src/i18n/locales/zh-Hans.ts +1 -1
  19. package/src/i18n/locales/zh-Hant.po +733 -522
  20. package/src/i18n/locales/zh-Hant.ts +1 -1
  21. package/src/i18n/middleware.ts +7 -11
  22. package/src/index.ts +1 -1
  23. package/src/lib/__tests__/icons.test.ts +178 -0
  24. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  25. package/src/lib/__tests__/schemas.test.ts +12 -6
  26. package/src/lib/__tests__/theme.test.ts +62 -0
  27. package/src/lib/__tests__/timezones.test.ts +1 -1
  28. package/src/lib/__tests__/url.test.ts +12 -0
  29. package/src/lib/__tests__/view.test.ts +1 -5
  30. package/src/lib/avatar-upload.ts +18 -10
  31. package/src/lib/collection-form-bridge.ts +52 -0
  32. package/src/lib/collections-reorder.ts +28 -0
  33. package/src/lib/compose-bridge.ts +251 -0
  34. package/src/lib/errors.ts +116 -0
  35. package/src/lib/excerpt.ts +1 -1
  36. package/src/lib/favicon.ts +3 -5
  37. package/src/lib/html.ts +22 -0
  38. package/src/lib/icon-catalog.ts +181 -0
  39. package/src/lib/icons.ts +202 -0
  40. package/src/lib/navigation.ts +18 -33
  41. package/src/lib/pagination.ts +3 -2
  42. package/src/lib/post-form-bridge.ts +136 -0
  43. package/src/lib/render.tsx +11 -4
  44. package/src/lib/resolve-config.ts +157 -0
  45. package/src/lib/schemas.ts +76 -12
  46. package/src/lib/settings-bridge.ts +139 -0
  47. package/src/lib/storage.ts +37 -16
  48. package/src/lib/theme.ts +5 -7
  49. package/src/lib/timeline.ts +4 -8
  50. package/src/lib/toast.ts +134 -0
  51. package/src/lib/upload.ts +71 -0
  52. package/src/lib/url.ts +9 -1
  53. package/src/lib/version.ts +16 -0
  54. package/src/lib/view.ts +9 -10
  55. package/src/middleware/__tests__/auth.test.ts +6 -28
  56. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  57. package/src/middleware/auth.ts +6 -12
  58. package/src/middleware/config.ts +51 -0
  59. package/src/middleware/error-handler.ts +56 -0
  60. package/src/middleware/onboarding.ts +1 -1
  61. package/src/preset.css +6 -0
  62. package/src/routes/__tests__/compose.test.ts +104 -17
  63. package/src/routes/api/__tests__/collections.test.ts +93 -2
  64. package/src/routes/api/__tests__/posts.test.ts +2 -1
  65. package/src/routes/api/__tests__/settings.test.ts +1 -1
  66. package/src/routes/api/collections.ts +64 -68
  67. package/src/routes/api/nav-items.ts +21 -59
  68. package/src/routes/api/pages.ts +18 -46
  69. package/src/routes/api/posts.ts +64 -86
  70. package/src/routes/api/search.ts +6 -4
  71. package/src/routes/api/settings.ts +8 -24
  72. package/src/routes/api/upload.ts +55 -53
  73. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  74. package/src/routes/auth/reset.tsx +17 -66
  75. package/src/routes/auth/setup.tsx +67 -11
  76. package/src/routes/auth/signin.tsx +44 -8
  77. package/src/routes/compose.tsx +194 -0
  78. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  79. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  80. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  81. package/src/routes/dash/appearance.tsx +173 -0
  82. package/src/routes/dash/collections.tsx +80 -14
  83. package/src/routes/dash/index.tsx +12 -14
  84. package/src/routes/dash/media.tsx +46 -49
  85. package/src/routes/dash/pages.tsx +85 -37
  86. package/src/routes/dash/posts.tsx +60 -23
  87. package/src/routes/dash/redirects.tsx +43 -33
  88. package/src/routes/dash/settings.tsx +234 -214
  89. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  90. package/src/routes/feed/rss.ts +11 -16
  91. package/src/routes/feed/sitemap.ts +15 -9
  92. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  93. package/src/routes/pages/archive.tsx +2 -2
  94. package/src/routes/pages/collection.tsx +76 -9
  95. package/src/routes/pages/collections.tsx +3 -1
  96. package/src/routes/pages/featured.tsx +2 -2
  97. package/src/routes/pages/home.tsx +3 -3
  98. package/src/routes/pages/latest.tsx +2 -2
  99. package/src/routes/pages/page.tsx +2 -2
  100. package/src/routes/pages/post.tsx +2 -2
  101. package/src/routes/pages/search.tsx +2 -2
  102. package/src/services/__tests__/collection.test.ts +324 -34
  103. package/src/services/__tests__/media.test.ts +1 -1
  104. package/src/services/__tests__/page.test.ts +116 -1
  105. package/src/services/auth.ts +88 -0
  106. package/src/services/collection.ts +169 -30
  107. package/src/services/index.ts +8 -3
  108. package/src/services/media.ts +39 -12
  109. package/src/services/navigation.ts +17 -5
  110. package/src/services/page.ts +24 -4
  111. package/src/services/post.ts +87 -19
  112. package/src/services/search.ts +0 -1
  113. package/src/services/settings.ts +21 -13
  114. package/src/style.css +3 -0
  115. package/src/styles/components.css +42 -1
  116. package/src/styles/tokens.css +4 -0
  117. package/src/styles/ui.css +902 -73
  118. package/src/types/app-context.ts +25 -0
  119. package/src/types/bindings.ts +1 -0
  120. package/src/types/config.ts +60 -23
  121. package/src/types/entities.ts +12 -2
  122. package/src/types/lingui-react-macro.d.ts +3 -3
  123. package/src/types/operations.ts +2 -4
  124. package/src/types/views.ts +1 -3
  125. package/src/ui/__tests__/font-themes.test.ts +27 -8
  126. package/src/ui/color-themes.ts +1 -1
  127. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  128. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  129. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  130. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  131. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  132. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  133. package/src/ui/components/collection-types.ts +45 -0
  134. package/src/ui/components/compose-types.ts +75 -0
  135. package/src/ui/components/jant-collection-form.ts +512 -0
  136. package/src/ui/components/jant-compose-dialog.ts +494 -0
  137. package/src/ui/components/jant-compose-editor.ts +799 -0
  138. package/src/ui/components/jant-post-form.ts +290 -0
  139. package/src/ui/components/jant-settings-avatar.ts +231 -0
  140. package/src/ui/components/jant-settings-general.ts +436 -0
  141. package/src/ui/components/post-form-template.ts +260 -0
  142. package/src/ui/components/post-form-types.ts +87 -0
  143. package/src/ui/components/settings-types.ts +62 -0
  144. package/src/ui/compose/ComposeDialog.tsx +141 -385
  145. package/src/ui/compose/ComposePrompt.tsx +3 -3
  146. package/src/ui/dash/PostList.tsx +55 -61
  147. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  148. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  149. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  150. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  151. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  152. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  153. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  154. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  155. package/src/ui/dash/index.ts +1 -1
  156. package/src/ui/dash/posts/PostForm.tsx +248 -0
  157. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  158. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  159. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  160. package/src/ui/font-themes.ts +115 -32
  161. package/src/ui/layouts/BaseLayout.tsx +49 -19
  162. package/src/ui/layouts/DashLayout.tsx +14 -9
  163. package/src/ui/layouts/SiteLayout.tsx +38 -23
  164. package/src/ui/pages/CollectionPage.tsx +12 -2
  165. package/src/ui/pages/CollectionsPage.tsx +27 -27
  166. package/src/ui/pages/HomePage.tsx +15 -6
  167. package/src/ui/pages/SearchPage.tsx +1 -2
  168. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  169. package/src/ui/shared/Pagination.tsx +2 -2
  170. package/dist/app.js +0 -267
  171. package/dist/auth.js +0 -39
  172. package/dist/client.js +0 -13
  173. package/dist/db/index.js +0 -10
  174. package/dist/db/schema.js +0 -224
  175. package/dist/i18n/Trans.js +0 -24
  176. package/dist/i18n/context.js +0 -58
  177. package/dist/i18n/detect.js +0 -26
  178. package/dist/i18n/i18n.js +0 -49
  179. package/dist/i18n/index.js +0 -44
  180. package/dist/i18n/locales/en.js +0 -1
  181. package/dist/i18n/locales/zh-Hans.js +0 -1
  182. package/dist/i18n/locales/zh-Hant.js +0 -1
  183. package/dist/i18n/locales.js +0 -13
  184. package/dist/i18n/middleware.js +0 -30
  185. package/dist/lib/avatar-upload.js +0 -134
  186. package/dist/lib/config.js +0 -143
  187. package/dist/lib/constants.js +0 -50
  188. package/dist/lib/excerpt.js +0 -76
  189. package/dist/lib/favicon.js +0 -102
  190. package/dist/lib/feed.js +0 -123
  191. package/dist/lib/image-processor.js +0 -187
  192. package/dist/lib/image.js +0 -97
  193. package/dist/lib/index.js +0 -7
  194. package/dist/lib/markdown.js +0 -83
  195. package/dist/lib/media-helpers.js +0 -49
  196. package/dist/lib/media-upload.js +0 -104
  197. package/dist/lib/nav-reorder.js +0 -27
  198. package/dist/lib/navigation.js +0 -79
  199. package/dist/lib/pagination.js +0 -44
  200. package/dist/lib/render.js +0 -53
  201. package/dist/lib/schemas.js +0 -174
  202. package/dist/lib/sqid.js +0 -72
  203. package/dist/lib/sse.js +0 -218
  204. package/dist/lib/storage.js +0 -164
  205. package/dist/lib/theme.js +0 -65
  206. package/dist/lib/time.js +0 -159
  207. package/dist/lib/timeline.js +0 -95
  208. package/dist/lib/timezones.js +0 -388
  209. package/dist/lib/url.js +0 -89
  210. package/dist/lib/view.js +0 -217
  211. package/dist/middleware/auth.js +0 -52
  212. package/dist/middleware/onboarding.js +0 -41
  213. package/dist/routes/api/collections.js +0 -124
  214. package/dist/routes/api/nav-items.js +0 -104
  215. package/dist/routes/api/pages.js +0 -91
  216. package/dist/routes/api/posts.js +0 -218
  217. package/dist/routes/api/search.js +0 -48
  218. package/dist/routes/api/settings.js +0 -68
  219. package/dist/routes/api/upload.js +0 -246
  220. package/dist/routes/auth/reset.js +0 -221
  221. package/dist/routes/auth/setup.js +0 -194
  222. package/dist/routes/auth/signin.js +0 -176
  223. package/dist/routes/compose.js +0 -48
  224. package/dist/routes/dash/collections.js +0 -115
  225. package/dist/routes/dash/index.js +0 -118
  226. package/dist/routes/dash/media.js +0 -106
  227. package/dist/routes/dash/pages.js +0 -294
  228. package/dist/routes/dash/posts.js +0 -244
  229. package/dist/routes/dash/redirects.js +0 -257
  230. package/dist/routes/dash/settings.js +0 -379
  231. package/dist/routes/feed/rss.js +0 -62
  232. package/dist/routes/feed/sitemap.js +0 -49
  233. package/dist/routes/pages/archive.js +0 -62
  234. package/dist/routes/pages/collection.js +0 -34
  235. package/dist/routes/pages/collections.js +0 -28
  236. package/dist/routes/pages/featured.js +0 -36
  237. package/dist/routes/pages/home.js +0 -64
  238. package/dist/routes/pages/latest.js +0 -45
  239. package/dist/routes/pages/page.js +0 -68
  240. package/dist/routes/pages/post.js +0 -44
  241. package/dist/routes/pages/search.js +0 -54
  242. package/dist/services/collection.js +0 -109
  243. package/dist/services/index.js +0 -24
  244. package/dist/services/media.js +0 -117
  245. package/dist/services/navigation.js +0 -91
  246. package/dist/services/page.js +0 -84
  247. package/dist/services/post.js +0 -229
  248. package/dist/services/redirect.js +0 -48
  249. package/dist/services/search.js +0 -67
  250. package/dist/services/settings.js +0 -68
  251. package/dist/types/bindings.js +0 -3
  252. package/dist/types/config.js +0 -147
  253. package/dist/types/constants.js +0 -27
  254. package/dist/types/entities.js +0 -3
  255. package/dist/types/lingui-react-macro.d.js +0 -9
  256. package/dist/types/operations.js +0 -3
  257. package/dist/types/props.js +0 -3
  258. package/dist/types/sortablejs.d.js +0 -5
  259. package/dist/types/views.js +0 -5
  260. package/dist/types.js +0 -11
  261. package/dist/ui/color-themes.js +0 -268
  262. package/dist/ui/compose/ComposeDialog.js +0 -467
  263. package/dist/ui/compose/ComposePrompt.js +0 -55
  264. package/dist/ui/dash/ActionButtons.js +0 -46
  265. package/dist/ui/dash/CrudPageHeader.js +0 -22
  266. package/dist/ui/dash/DangerZone.js +0 -36
  267. package/dist/ui/dash/FormatBadge.js +0 -27
  268. package/dist/ui/dash/ListItemRow.js +0 -21
  269. package/dist/ui/dash/PageForm.js +0 -195
  270. package/dist/ui/dash/PostForm.js +0 -395
  271. package/dist/ui/dash/PostList.js +0 -83
  272. package/dist/ui/dash/StatusBadge.js +0 -46
  273. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  274. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  275. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  276. package/dist/ui/dash/index.js +0 -10
  277. package/dist/ui/dash/media/MediaListContent.js +0 -166
  278. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  279. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  280. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  281. package/dist/ui/dash/settings/AccountContent.js +0 -209
  282. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  283. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  284. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  285. package/dist/ui/feed/LinkCard.js +0 -72
  286. package/dist/ui/feed/NoteCard.js +0 -58
  287. package/dist/ui/feed/QuoteCard.js +0 -63
  288. package/dist/ui/feed/ThreadPreview.js +0 -48
  289. package/dist/ui/feed/TimelineFeed.js +0 -41
  290. package/dist/ui/feed/TimelineItem.js +0 -27
  291. package/dist/ui/font-themes.js +0 -36
  292. package/dist/ui/layouts/BaseLayout.js +0 -153
  293. package/dist/ui/layouts/DashLayout.js +0 -141
  294. package/dist/ui/layouts/SiteLayout.js +0 -169
  295. package/dist/ui/pages/ArchivePage.js +0 -143
  296. package/dist/ui/pages/CollectionPage.js +0 -70
  297. package/dist/ui/pages/CollectionsPage.js +0 -76
  298. package/dist/ui/pages/FeaturedPage.js +0 -24
  299. package/dist/ui/pages/HomePage.js +0 -24
  300. package/dist/ui/pages/PostPage.js +0 -55
  301. package/dist/ui/pages/SearchPage.js +0 -122
  302. package/dist/ui/pages/SinglePage.js +0 -23
  303. package/dist/ui/shared/EmptyState.js +0 -27
  304. package/dist/ui/shared/MediaGallery.js +0 -35
  305. package/dist/ui/shared/Pagination.js +0 -195
  306. package/dist/ui/shared/ThreadView.js +0 -108
  307. package/dist/ui/shared/index.js +0 -5
  308. package/dist/vendor/datastar.js +0 -1606
  309. package/src/lib/__tests__/config.test.ts +0 -192
  310. package/src/lib/config.ts +0 -167
  311. package/src/routes/compose.ts +0 -63
  312. package/src/ui/dash/PostForm.tsx +0 -360
  313. package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
@@ -1,84 +0,0 @@
1
- /**
2
- * Page Service
3
- *
4
- * CRUD operations for standalone pages (about, now, etc.)
5
- */ import { eq, desc, sql } from "drizzle-orm";
6
- import { pages, navItems } from "../db/schema.js";
7
- import { now } from "../lib/time.js";
8
- import { render as renderMarkdown } from "../lib/markdown.js";
9
- export function createPageService(db) {
10
- function toPage(row) {
11
- return {
12
- id: row.id,
13
- slug: row.slug,
14
- title: row.title,
15
- body: row.body,
16
- bodyHtml: row.bodyHtml,
17
- status: row.status,
18
- createdAt: row.createdAt,
19
- updatedAt: row.updatedAt
20
- };
21
- }
22
- return {
23
- async getById (id) {
24
- const result = await db.select().from(pages).where(eq(pages.id, id)).limit(1);
25
- return result[0] ? toPage(result[0]) : null;
26
- },
27
- async getBySlug (slug) {
28
- const result = await db.select().from(pages).where(eq(pages.slug, slug)).limit(1);
29
- return result[0] ? toPage(result[0]) : null;
30
- },
31
- async list () {
32
- const rows = await db.select().from(pages).orderBy(desc(pages.createdAt));
33
- return rows.map(toPage);
34
- },
35
- async listNotInNav () {
36
- const rows = await db.select().from(pages).where(sql`${pages.id} NOT IN (SELECT ${navItems.pageId} FROM ${navItems} WHERE ${navItems.pageId} IS NOT NULL)`).orderBy(desc(pages.createdAt));
37
- return rows.map(toPage);
38
- },
39
- async create (data) {
40
- const timestamp = now();
41
- const bodyHtml = data.body ? renderMarkdown(data.body) : null;
42
- const result = await db.insert(pages).values({
43
- slug: data.slug,
44
- title: data.title ?? null,
45
- body: data.body ?? null,
46
- bodyHtml,
47
- status: data.status ?? "published",
48
- createdAt: timestamp,
49
- updatedAt: timestamp
50
- }).returning();
51
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
52
- return toPage(result[0]);
53
- },
54
- async update (id, data) {
55
- const existing = await this.getById(id);
56
- if (!existing) return null;
57
- const timestamp = now();
58
- const updates = {
59
- updatedAt: timestamp
60
- };
61
- if (data.slug !== undefined) updates.slug = data.slug;
62
- if (data.title !== undefined) updates.title = data.title;
63
- if (data.status !== undefined) updates.status = data.status;
64
- if (data.body !== undefined) {
65
- updates.body = data.body;
66
- updates.bodyHtml = data.body ? renderMarkdown(data.body) : null;
67
- }
68
- // If slug changed, update related nav_items
69
- if (data.slug !== undefined && data.slug !== existing.slug) {
70
- await db.update(navItems).set({
71
- url: `/${data.slug}`,
72
- updatedAt: timestamp
73
- }).where(eq(navItems.pageId, id));
74
- }
75
- const result = await db.update(pages).set(updates).where(eq(pages.id, id)).returning();
76
- return result[0] ? toPage(result[0]) : null;
77
- },
78
- async delete (id) {
79
- // nav_items with page_id FK have ON DELETE CASCADE, so they auto-delete
80
- const result = await db.delete(pages).where(eq(pages.id, id)).returning();
81
- return result.length > 0;
82
- }
83
- };
84
- }
@@ -1,229 +0,0 @@
1
- /**
2
- * Post Service (v2)
3
- *
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";
8
- import { posts } from "../db/schema.js";
9
- import { now } from "../lib/time.js";
10
- import { render as renderMarkdown } from "../lib/markdown.js";
11
- export function createPostService(db) {
12
- /** Build WHERE conditions from filters (shared by list and count) */ function buildFilterConditions(filters) {
13
- const conditions = [];
14
- if (filters.status) {
15
- conditions.push(eq(posts.status, filters.status));
16
- }
17
- if (filters.featured !== undefined) {
18
- conditions.push(eq(posts.featured, filters.featured ? 1 : 0));
19
- }
20
- if (filters.pinned !== undefined) {
21
- conditions.push(eq(posts.pinned, filters.pinned ? 1 : 0));
22
- }
23
- if (filters.format) {
24
- conditions.push(eq(posts.format, filters.format));
25
- }
26
- if (filters.collectionId !== undefined) {
27
- conditions.push(eq(posts.collectionId, filters.collectionId));
28
- }
29
- if (filters.threadId) {
30
- conditions.push(eq(posts.threadId, filters.threadId));
31
- }
32
- if (filters.excludeReplies) {
33
- conditions.push(isNull(posts.threadId));
34
- }
35
- if (!filters.includeDeleted) {
36
- conditions.push(isNull(posts.deletedAt));
37
- }
38
- return conditions;
39
- }
40
- function toPost(row) {
41
- return {
42
- id: row.id,
43
- format: row.format,
44
- status: row.status,
45
- featured: row.featured,
46
- pinned: row.pinned,
47
- path: row.path,
48
- title: row.title,
49
- url: row.url,
50
- body: row.body,
51
- bodyHtml: row.bodyHtml,
52
- quoteText: row.quoteText,
53
- rating: row.rating,
54
- collectionId: row.collectionId,
55
- replyToId: row.replyToId,
56
- threadId: row.threadId,
57
- deletedAt: row.deletedAt,
58
- publishedAt: row.publishedAt,
59
- createdAt: row.createdAt,
60
- updatedAt: row.updatedAt
61
- };
62
- }
63
- return {
64
- async getById (id) {
65
- const result = await db.select().from(posts).where(and(eq(posts.id, id), isNull(posts.deletedAt))).limit(1);
66
- return result[0] ? toPost(result[0]) : null;
67
- },
68
- async getByPath (path) {
69
- const result = await db.select().from(posts).where(and(eq(posts.path, path), isNull(posts.deletedAt))).limit(1);
70
- return result[0] ? toPost(result[0]) : null;
71
- },
72
- async list (filters = {}) {
73
- const conditions = buildFilterConditions(filters);
74
- if (filters.cursor) {
75
- conditions.push(sql`${posts.id} < ${filters.cursor}`);
76
- }
77
- let query = db.select().from(posts).where(conditions.length > 0 ? and(...conditions) : undefined).orderBy(desc(posts.publishedAt), desc(posts.id)).limit(filters.limit ?? 100);
78
- if (filters.offset !== undefined) {
79
- query = query.offset(filters.offset);
80
- }
81
- const rows = await query;
82
- return rows.map(toPost);
83
- },
84
- async count (filters = {}) {
85
- const conditions = buildFilterConditions(filters);
86
- const result = await db.select({
87
- count: sql`count(*)`.as("count")
88
- }).from(posts).where(conditions.length > 0 ? and(...conditions) : undefined);
89
- return result[0]?.count ?? 0;
90
- },
91
- async create (data) {
92
- const timestamp = now();
93
- const bodyHtml = data.body ? renderMarkdown(data.body) : null;
94
- // Handle thread relationship
95
- let threadId = null;
96
- let status = data.status ?? "published";
97
- let featured = data.featured ?? false;
98
- if (data.replyToId) {
99
- const parent = await this.getById(data.replyToId);
100
- if (parent) {
101
- threadId = parent.threadId ?? parent.id;
102
- // Inherit status and featured from root
103
- const root = parent.threadId ? await this.getById(parent.threadId) : parent;
104
- if (root) {
105
- status = root.status;
106
- featured = root.featured === 1;
107
- }
108
- }
109
- }
110
- const result = await db.insert(posts).values({
111
- format: data.format,
112
- status,
113
- featured: featured ? 1 : 0,
114
- pinned: data.pinned ? 1 : 0,
115
- path: data.path ?? null,
116
- title: data.title ?? null,
117
- url: data.url ?? null,
118
- body: data.body ?? null,
119
- bodyHtml,
120
- quoteText: data.quoteText ?? null,
121
- rating: data.rating ?? null,
122
- collectionId: data.collectionId ?? null,
123
- replyToId: data.replyToId ?? null,
124
- threadId,
125
- publishedAt: data.publishedAt ?? timestamp,
126
- createdAt: timestamp,
127
- updatedAt: timestamp
128
- }).returning();
129
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
130
- return toPost(result[0]);
131
- },
132
- async update (id, data) {
133
- const existing = await this.getById(id);
134
- if (!existing) return null;
135
- const timestamp = now();
136
- const updates = {
137
- updatedAt: timestamp
138
- };
139
- if (data.format !== undefined) updates.format = data.format;
140
- if (data.path !== undefined) updates.path = data.path;
141
- if (data.title !== undefined) updates.title = data.title;
142
- if (data.url !== undefined) updates.url = data.url;
143
- if (data.quoteText !== undefined) updates.quoteText = data.quoteText;
144
- if (data.rating !== undefined) updates.rating = data.rating;
145
- if (data.collectionId !== undefined) updates.collectionId = data.collectionId;
146
- if (data.publishedAt !== undefined) updates.publishedAt = data.publishedAt;
147
- if (data.pinned !== undefined) updates.pinned = data.pinned ? 1 : 0;
148
- if (data.body !== undefined) {
149
- updates.body = data.body;
150
- updates.bodyHtml = data.body ? renderMarkdown(data.body) : null;
151
- }
152
- // Handle status/featured change - cascade to thread if this is root
153
- const statusChanged = data.status !== undefined && data.status !== existing.status;
154
- const featuredChanged = data.featured !== undefined && (data.featured ? 1 : 0) !== existing.featured;
155
- if (statusChanged) updates.status = data.status;
156
- if (featuredChanged) updates.featured = data.featured ? 1 : 0;
157
- // If this is a root post and status/featured changed, cascade to thread
158
- if ((statusChanged || featuredChanged) && !existing.threadId) {
159
- await this.updateThreadStatusAndFeatured(id, data.status ?? existing.status, data.featured !== undefined ? data.featured : existing.featured === 1);
160
- }
161
- const result = await db.update(posts).set(updates).where(eq(posts.id, id)).returning();
162
- return result[0] ? toPost(result[0]) : null;
163
- },
164
- async delete (id) {
165
- const existing = await this.getById(id);
166
- if (!existing) return false;
167
- const timestamp = now();
168
- // If this is a thread root, soft delete all posts in the thread
169
- if (!existing.threadId) {
170
- await db.update(posts).set({
171
- deletedAt: timestamp,
172
- updatedAt: timestamp
173
- }).where(or(eq(posts.id, id), eq(posts.threadId, id)));
174
- } else {
175
- await db.update(posts).set({
176
- deletedAt: timestamp,
177
- updatedAt: timestamp
178
- }).where(eq(posts.id, id));
179
- }
180
- return true;
181
- },
182
- async getThread (rootId) {
183
- const rows = await db.select().from(posts).where(and(or(eq(posts.id, rootId), eq(posts.threadId, rootId)), isNull(posts.deletedAt))).orderBy(posts.createdAt);
184
- return rows.map(toPost);
185
- },
186
- async updateThreadStatusAndFeatured (rootId, status, featured) {
187
- const timestamp = now();
188
- await db.update(posts).set({
189
- status,
190
- featured: featured ? 1 : 0,
191
- updatedAt: timestamp
192
- }).where(eq(posts.threadId, rootId));
193
- },
194
- async getReplyCounts (postIds) {
195
- if (postIds.length === 0) return new Map();
196
- const rows = await db.select({
197
- threadId: posts.threadId,
198
- count: sql`count(*)`.as("count")
199
- }).from(posts).where(and(inArray(posts.threadId, postIds), isNull(posts.deletedAt))).groupBy(posts.threadId);
200
- const counts = new Map();
201
- for (const row of rows){
202
- if (row.threadId !== null) {
203
- counts.set(row.threadId, row.count);
204
- }
205
- }
206
- return counts;
207
- },
208
- async getThreadPreviews (rootIds, previewCount = 3) {
209
- if (rootIds.length === 0) return new Map();
210
- const rows = await db.select().from(posts).where(and(inArray(posts.threadId, rootIds), isNull(posts.deletedAt))).orderBy(posts.threadId, posts.createdAt);
211
- const result = new Map();
212
- for (const row of rows){
213
- const post = toPost(row);
214
- if (post.threadId === null) continue;
215
- const list = result.get(post.threadId);
216
- if (list) {
217
- if (list.length < previewCount) {
218
- list.push(post);
219
- }
220
- } else {
221
- result.set(post.threadId, [
222
- post
223
- ]);
224
- }
225
- }
226
- return result;
227
- }
228
- };
229
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * Redirect Service
3
- *
4
- * URL redirect management for path changes
5
- */ import { eq } from "drizzle-orm";
6
- import { redirects } from "../db/schema.js";
7
- import { now } from "../lib/time.js";
8
- import { normalizePath } from "../lib/url.js";
9
- export function createRedirectService(db) {
10
- function toRedirect(row) {
11
- return {
12
- id: row.id,
13
- fromPath: row.fromPath,
14
- toPath: row.toPath,
15
- type: row.type,
16
- createdAt: row.createdAt
17
- };
18
- }
19
- return {
20
- async getByPath (fromPath) {
21
- const normalized = normalizePath(fromPath);
22
- const result = await db.select().from(redirects).where(eq(redirects.fromPath, normalized)).limit(1);
23
- return result[0] ? toRedirect(result[0]) : null;
24
- },
25
- async create (fromPath, toPath, type = 301) {
26
- const timestamp = now();
27
- const normalizedFrom = normalizePath(fromPath);
28
- // Delete existing redirect from this path if any
29
- await db.delete(redirects).where(eq(redirects.fromPath, normalizedFrom));
30
- const result = await db.insert(redirects).values({
31
- fromPath: normalizedFrom,
32
- toPath,
33
- type,
34
- createdAt: timestamp
35
- }).returning();
36
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
37
- return toRedirect(result[0]);
38
- },
39
- async delete (id) {
40
- const result = await db.delete(redirects).where(eq(redirects.id, id)).returning();
41
- return result.length > 0;
42
- },
43
- async list () {
44
- const rows = await db.select().from(redirects);
45
- return rows.map(toRedirect);
46
- }
47
- };
48
- }
@@ -1,67 +0,0 @@
1
- /**
2
- * Search Service (v2)
3
- *
4
- * Full-text search using FTS5
5
- */ export function createSearchService(d1) {
6
- return {
7
- async search (query, options = {}) {
8
- const limit = options.limit ?? 20;
9
- const offset = options.offset ?? 0;
10
- const status = options.status ?? [
11
- "published"
12
- ];
13
- // Escape and prepare the query for FTS5
14
- const ftsQuery = query.trim().split(/\s+/).filter((term)=>term.length > 0).map((term)=>`"${term.replace(/"/g, '""')}"*`).join(" ");
15
- if (!ftsQuery) {
16
- return [];
17
- }
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
- ] : [];
25
- const stmt = d1.prepare(`
26
- SELECT
27
- posts.*,
28
- posts_fts.rank AS rank,
29
- snippet(posts_fts, 1, '<mark>', '</mark>', '...', 32) AS snippet
30
- FROM posts_fts
31
- JOIN posts ON posts.id = posts_fts.rowid
32
- WHERE posts_fts MATCH ?
33
- AND posts.deleted_at IS NULL
34
- AND posts.status IN (${statusPlaceholders})
35
- ${formatFilter}
36
- ORDER BY posts_fts.rank
37
- LIMIT ? OFFSET ?
38
- `);
39
- const { results } = await stmt.bind(ftsQuery, ...status, ...formatParams, limit, offset).all();
40
- return (results || []).map((row)=>({
41
- post: {
42
- id: row.id,
43
- format: row.format,
44
- status: row.status,
45
- featured: row.featured,
46
- pinned: row.pinned,
47
- path: row.path,
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,
55
- replyToId: row.reply_to_id,
56
- threadId: row.thread_id,
57
- deletedAt: row.deleted_at,
58
- publishedAt: row.published_at,
59
- createdAt: row.created_at,
60
- updatedAt: row.updated_at
61
- },
62
- rank: row.rank,
63
- snippet: row.snippet
64
- }));
65
- }
66
- };
67
- }
@@ -1,68 +0,0 @@
1
- /**
2
- * Settings Service
3
- *
4
- * Key-value store for site configuration
5
- */ import { eq } from "drizzle-orm";
6
- import { settings } from "../db/schema.js";
7
- import { now } from "../lib/time.js";
8
- import { SETTINGS_KEYS, ONBOARDING_STATUS } from "../lib/constants.js";
9
- export function createSettingsService(db) {
10
- return {
11
- async get (key) {
12
- const result = await db.select().from(settings).where(eq(settings.key, key)).limit(1);
13
- return result[0]?.value ?? null;
14
- },
15
- async getAll () {
16
- const rows = await db.select().from(settings);
17
- const result = {};
18
- for (const row of rows){
19
- result[row.key] = row.value;
20
- }
21
- return result;
22
- },
23
- async set (key, value) {
24
- const timestamp = now();
25
- await db.insert(settings).values({
26
- key,
27
- value,
28
- updatedAt: timestamp
29
- }).onConflictDoUpdate({
30
- target: settings.key,
31
- set: {
32
- value,
33
- updatedAt: timestamp
34
- }
35
- });
36
- },
37
- async remove (key) {
38
- await db.delete(settings).where(eq(settings.key, key));
39
- },
40
- async setMany (entries) {
41
- const timestamp = now();
42
- const keys = Object.keys(entries);
43
- for (const key of keys){
44
- const value = entries[key];
45
- if (value !== undefined) {
46
- await db.insert(settings).values({
47
- key,
48
- value,
49
- updatedAt: timestamp
50
- }).onConflictDoUpdate({
51
- target: settings.key,
52
- set: {
53
- value,
54
- updatedAt: timestamp
55
- }
56
- });
57
- }
58
- }
59
- },
60
- async isOnboardingComplete () {
61
- const status = await this.get(SETTINGS_KEYS.ONBOARDING_STATUS);
62
- return status === ONBOARDING_STATUS.COMPLETED;
63
- },
64
- async completeOnboarding () {
65
- await this.set(SETTINGS_KEYS.ONBOARDING_STATUS, ONBOARDING_STATUS.COMPLETED);
66
- }
67
- };
68
- }
@@ -1,3 +0,0 @@
1
- /**
2
- * Cloudflare Worker Bindings
3
- */ export { };
@@ -1,147 +0,0 @@
1
- /**
2
- * Configuration System
3
- *
4
- * Single Source of Truth for all configuration fields.
5
- */ /**
6
- * Configuration Registry - Single Source of Truth
7
- *
8
- * All available configuration fields with their metadata.
9
- * Add new fields here, and they'll automatically work everywhere.
10
- *
11
- * Priority logic:
12
- * - envOnly: false -> User-configurable (DB > ENV > Default)
13
- * - envOnly: true -> Environment-only (ENV > Default)
14
- */ export const CONFIG_FIELDS = {
15
- // User-configurable (can be modified in dashboard)
16
- SITE_NAME: {
17
- defaultValue: "Jant",
18
- envOnly: false
19
- },
20
- SITE_DESCRIPTION: {
21
- defaultValue: "A microblog powered by Jant",
22
- envOnly: false
23
- },
24
- SITE_LANGUAGE: {
25
- defaultValue: "en",
26
- envOnly: false
27
- },
28
- HOME_DEFAULT_VIEW: {
29
- defaultValue: "latest",
30
- envOnly: false
31
- },
32
- // Environment-only (deployment/infrastructure config)
33
- SITE_URL: {
34
- defaultValue: "",
35
- envOnly: true
36
- },
37
- AUTH_SECRET: {
38
- defaultValue: "",
39
- envOnly: true
40
- },
41
- R2_PUBLIC_URL: {
42
- defaultValue: "",
43
- envOnly: true
44
- },
45
- IMAGE_TRANSFORM_URL: {
46
- defaultValue: "",
47
- envOnly: true
48
- },
49
- DEMO_EMAIL: {
50
- defaultValue: "",
51
- envOnly: true
52
- },
53
- DEMO_PASSWORD: {
54
- defaultValue: "",
55
- envOnly: true
56
- },
57
- PAGE_SIZE: {
58
- defaultValue: "20",
59
- envOnly: true
60
- },
61
- STORAGE_DRIVER: {
62
- defaultValue: "r2",
63
- envOnly: true
64
- },
65
- S3_ENDPOINT: {
66
- defaultValue: "",
67
- envOnly: true
68
- },
69
- S3_BUCKET: {
70
- defaultValue: "",
71
- envOnly: true
72
- },
73
- S3_ACCESS_KEY_ID: {
74
- defaultValue: "",
75
- envOnly: true
76
- },
77
- S3_SECRET_ACCESS_KEY: {
78
- defaultValue: "",
79
- envOnly: true
80
- },
81
- S3_REGION: {
82
- defaultValue: "auto",
83
- envOnly: true
84
- },
85
- S3_PUBLIC_URL: {
86
- defaultValue: "",
87
- envOnly: true
88
- },
89
- // Internal settings (DB-only, not configurable via env or dashboard)
90
- THEME: {
91
- defaultValue: "",
92
- envOnly: false,
93
- internal: true
94
- },
95
- CUSTOM_CSS: {
96
- defaultValue: "",
97
- envOnly: false,
98
- internal: true
99
- },
100
- SITE_AVATAR: {
101
- defaultValue: "",
102
- envOnly: false,
103
- internal: true
104
- },
105
- SHOW_HEADER_AVATAR: {
106
- defaultValue: "",
107
- envOnly: false,
108
- internal: true
109
- },
110
- SITE_FAVICON_ICO: {
111
- defaultValue: "",
112
- envOnly: false,
113
- internal: true
114
- },
115
- SITE_FAVICON_APPLE_TOUCH: {
116
- defaultValue: "",
117
- envOnly: false,
118
- internal: true
119
- },
120
- FONT_THEME: {
121
- defaultValue: "",
122
- envOnly: false,
123
- internal: true
124
- },
125
- TIME_ZONE: {
126
- defaultValue: "UTC",
127
- envOnly: false
128
- },
129
- SITE_FOOTER: {
130
- defaultValue: "",
131
- envOnly: false
132
- },
133
- NOINDEX: {
134
- defaultValue: "",
135
- envOnly: false
136
- },
137
- ONBOARDING_STATUS: {
138
- defaultValue: "pending",
139
- envOnly: false,
140
- internal: true
141
- },
142
- PASSWORD_RESET_TOKEN: {
143
- defaultValue: "",
144
- envOnly: false,
145
- internal: true
146
- }
147
- };
@@ -1,27 +0,0 @@
1
- /**
2
- * Content Type Constants
3
- */ export const FORMATS = [
4
- "note",
5
- "link",
6
- "quote"
7
- ];
8
- export const STATUSES = [
9
- "draft",
10
- "published"
11
- ];
12
- export const SORT_ORDERS = [
13
- "newest",
14
- "oldest",
15
- "rating_desc",
16
- "rating_asc"
17
- ];
18
- export const NAV_ITEM_TYPES = [
19
- "page",
20
- "link"
21
- ];
22
- export const MAX_MEDIA_ATTACHMENTS = 20;
23
- export const MAX_PINNED_POSTS = 3;
24
- export const STORAGE_DRIVERS = [
25
- "r2",
26
- "s3"
27
- ];
@@ -1,3 +0,0 @@
1
- /**
2
- * Entity Types (database-level models)
3
- */ export { };
@@ -1,9 +0,0 @@
1
- /**
2
- * Type declarations for @lingui/react/macro
3
- *
4
- * @lingui/react is not installed (it requires React as a peer dependency),
5
- * but the SWC Lingui plugin recognizes imports from @lingui/react/macro and
6
- * rewrites them via runtimeConfigModule to our custom Hono JSX implementation.
7
- *
8
- * These declarations satisfy TypeScript for the pre-transformation API surface.
9
- */