@jant/core 0.3.27 → 0.3.29

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 (314) hide show
  1. package/bin/reset-password.js +22 -0
  2. package/dist/client/client.css +1 -0
  3. package/dist/client/client.js +31561 -0
  4. package/dist/index.js +15209 -15
  5. package/package.json +25 -15
  6. package/src/__tests__/helpers/app.ts +19 -3
  7. package/src/__tests__/helpers/db.ts +44 -0
  8. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  9. package/src/app.tsx +111 -174
  10. package/src/client.ts +13 -0
  11. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  12. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  13. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  14. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  15. package/src/db/schema.ts +24 -4
  16. package/src/i18n/locales/en.po +810 -385
  17. package/src/i18n/locales/en.ts +1 -1
  18. package/src/i18n/locales/zh-Hans.po +733 -522
  19. package/src/i18n/locales/zh-Hans.ts +1 -1
  20. package/src/i18n/locales/zh-Hant.po +733 -522
  21. package/src/i18n/locales/zh-Hant.ts +1 -1
  22. package/src/i18n/middleware.ts +7 -11
  23. package/src/index.ts +1 -1
  24. package/src/lib/__tests__/icons.test.ts +178 -0
  25. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  26. package/src/lib/__tests__/schemas.test.ts +12 -6
  27. package/src/lib/__tests__/theme.test.ts +62 -0
  28. package/src/lib/__tests__/timezones.test.ts +1 -1
  29. package/src/lib/__tests__/url.test.ts +12 -0
  30. package/src/lib/__tests__/view.test.ts +1 -5
  31. package/src/lib/avatar-upload.ts +18 -10
  32. package/src/lib/collection-form-bridge.ts +52 -0
  33. package/src/lib/collections-reorder.ts +28 -0
  34. package/src/lib/compose-bridge.ts +251 -0
  35. package/src/lib/errors.ts +116 -0
  36. package/src/lib/excerpt.ts +1 -1
  37. package/src/lib/favicon.ts +3 -5
  38. package/src/lib/html.ts +22 -0
  39. package/src/lib/icon-catalog.ts +181 -0
  40. package/src/lib/icons.ts +202 -0
  41. package/src/lib/navigation.ts +18 -33
  42. package/src/lib/pagination.ts +3 -2
  43. package/src/lib/post-form-bridge.ts +136 -0
  44. package/src/lib/render.tsx +11 -4
  45. package/src/lib/resolve-config.ts +157 -0
  46. package/src/lib/schemas.ts +76 -12
  47. package/src/lib/settings-bridge.ts +139 -0
  48. package/src/lib/storage.ts +37 -16
  49. package/src/lib/theme.ts +5 -7
  50. package/src/lib/timeline.ts +4 -8
  51. package/src/lib/toast.ts +134 -0
  52. package/src/lib/upload.ts +71 -0
  53. package/src/lib/url.ts +9 -1
  54. package/src/lib/version.ts +16 -0
  55. package/src/lib/view.ts +9 -10
  56. package/src/middleware/__tests__/auth.test.ts +6 -28
  57. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  58. package/src/middleware/auth.ts +6 -12
  59. package/src/middleware/config.ts +51 -0
  60. package/src/middleware/error-handler.ts +56 -0
  61. package/src/middleware/onboarding.ts +1 -1
  62. package/src/preset.css +6 -0
  63. package/src/routes/__tests__/compose.test.ts +104 -17
  64. package/src/routes/api/__tests__/collections.test.ts +93 -2
  65. package/src/routes/api/__tests__/posts.test.ts +2 -1
  66. package/src/routes/api/__tests__/settings.test.ts +1 -1
  67. package/src/routes/api/collections.ts +64 -68
  68. package/src/routes/api/nav-items.ts +21 -59
  69. package/src/routes/api/pages.ts +18 -46
  70. package/src/routes/api/posts.ts +64 -86
  71. package/src/routes/api/search.ts +6 -4
  72. package/src/routes/api/settings.ts +8 -24
  73. package/src/routes/api/upload.ts +55 -53
  74. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  75. package/src/routes/auth/reset.tsx +17 -66
  76. package/src/routes/auth/setup.tsx +67 -11
  77. package/src/routes/auth/signin.tsx +44 -8
  78. package/src/routes/compose.tsx +194 -0
  79. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  80. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  81. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  82. package/src/routes/dash/appearance.tsx +173 -0
  83. package/src/routes/dash/collections.tsx +80 -14
  84. package/src/routes/dash/index.tsx +12 -14
  85. package/src/routes/dash/media.tsx +46 -49
  86. package/src/routes/dash/pages.tsx +85 -37
  87. package/src/routes/dash/posts.tsx +60 -23
  88. package/src/routes/dash/redirects.tsx +43 -33
  89. package/src/routes/dash/settings.tsx +234 -214
  90. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  91. package/src/routes/feed/rss.ts +11 -16
  92. package/src/routes/feed/sitemap.ts +15 -9
  93. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  94. package/src/routes/pages/archive.tsx +2 -2
  95. package/src/routes/pages/collection.tsx +76 -9
  96. package/src/routes/pages/collections.tsx +3 -1
  97. package/src/routes/pages/featured.tsx +2 -2
  98. package/src/routes/pages/home.tsx +3 -3
  99. package/src/routes/pages/latest.tsx +2 -2
  100. package/src/routes/pages/page.tsx +2 -2
  101. package/src/routes/pages/post.tsx +2 -2
  102. package/src/routes/pages/search.tsx +2 -2
  103. package/src/services/__tests__/collection.test.ts +324 -34
  104. package/src/services/__tests__/media.test.ts +1 -1
  105. package/src/services/__tests__/page.test.ts +116 -1
  106. package/src/services/auth.ts +88 -0
  107. package/src/services/collection.ts +169 -30
  108. package/src/services/index.ts +8 -3
  109. package/src/services/media.ts +39 -12
  110. package/src/services/navigation.ts +17 -5
  111. package/src/services/page.ts +24 -4
  112. package/src/services/post.ts +87 -19
  113. package/src/services/search.ts +0 -1
  114. package/src/services/settings.ts +21 -13
  115. package/src/style.css +3 -0
  116. package/src/styles/components.css +42 -1
  117. package/src/styles/tokens.css +4 -0
  118. package/src/styles/ui.css +902 -73
  119. package/src/types/app-context.ts +25 -0
  120. package/src/types/bindings.ts +1 -0
  121. package/src/types/config.ts +60 -23
  122. package/src/types/entities.ts +12 -2
  123. package/src/types/lingui-react-macro.d.ts +3 -3
  124. package/src/types/operations.ts +2 -4
  125. package/src/types/views.ts +1 -3
  126. package/src/ui/__tests__/font-themes.test.ts +27 -8
  127. package/src/ui/color-themes.ts +1 -1
  128. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  129. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  130. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  131. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  132. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  133. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  134. package/src/ui/components/collection-types.ts +45 -0
  135. package/src/ui/components/compose-types.ts +75 -0
  136. package/src/ui/components/jant-collection-form.ts +512 -0
  137. package/src/ui/components/jant-compose-dialog.ts +494 -0
  138. package/src/ui/components/jant-compose-editor.ts +799 -0
  139. package/src/ui/components/jant-post-form.ts +290 -0
  140. package/src/ui/components/jant-settings-avatar.ts +231 -0
  141. package/src/ui/components/jant-settings-general.ts +436 -0
  142. package/src/ui/components/post-form-template.ts +260 -0
  143. package/src/ui/components/post-form-types.ts +87 -0
  144. package/src/ui/components/settings-types.ts +62 -0
  145. package/src/ui/compose/ComposeDialog.tsx +141 -385
  146. package/src/ui/compose/ComposePrompt.tsx +3 -3
  147. package/src/ui/dash/PostList.tsx +55 -61
  148. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  149. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  150. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  151. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  152. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  153. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  154. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  155. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  156. package/src/ui/dash/index.ts +1 -1
  157. package/src/ui/dash/posts/PostForm.tsx +248 -0
  158. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  159. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  160. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  161. package/src/ui/font-themes.ts +115 -32
  162. package/src/ui/layouts/BaseLayout.tsx +49 -19
  163. package/src/ui/layouts/DashLayout.tsx +14 -9
  164. package/src/ui/layouts/SiteLayout.tsx +38 -23
  165. package/src/ui/pages/CollectionPage.tsx +12 -2
  166. package/src/ui/pages/CollectionsPage.tsx +27 -27
  167. package/src/ui/pages/HomePage.tsx +15 -6
  168. package/src/ui/pages/SearchPage.tsx +1 -2
  169. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  170. package/src/ui/shared/Pagination.tsx +2 -2
  171. package/dist/app.js +0 -267
  172. package/dist/auth.js +0 -39
  173. package/dist/client.js +0 -13
  174. package/dist/db/index.js +0 -10
  175. package/dist/db/schema.js +0 -224
  176. package/dist/i18n/Trans.js +0 -24
  177. package/dist/i18n/context.js +0 -58
  178. package/dist/i18n/detect.js +0 -26
  179. package/dist/i18n/i18n.js +0 -49
  180. package/dist/i18n/index.js +0 -44
  181. package/dist/i18n/locales/en.js +0 -1
  182. package/dist/i18n/locales/zh-Hans.js +0 -1
  183. package/dist/i18n/locales/zh-Hant.js +0 -1
  184. package/dist/i18n/locales.js +0 -13
  185. package/dist/i18n/middleware.js +0 -30
  186. package/dist/lib/avatar-upload.js +0 -134
  187. package/dist/lib/config.js +0 -143
  188. package/dist/lib/constants.js +0 -50
  189. package/dist/lib/excerpt.js +0 -76
  190. package/dist/lib/favicon.js +0 -102
  191. package/dist/lib/feed.js +0 -123
  192. package/dist/lib/image-processor.js +0 -187
  193. package/dist/lib/image.js +0 -97
  194. package/dist/lib/index.js +0 -7
  195. package/dist/lib/markdown.js +0 -83
  196. package/dist/lib/media-helpers.js +0 -49
  197. package/dist/lib/media-upload.js +0 -104
  198. package/dist/lib/nav-reorder.js +0 -27
  199. package/dist/lib/navigation.js +0 -79
  200. package/dist/lib/pagination.js +0 -44
  201. package/dist/lib/render.js +0 -53
  202. package/dist/lib/schemas.js +0 -174
  203. package/dist/lib/sqid.js +0 -72
  204. package/dist/lib/sse.js +0 -218
  205. package/dist/lib/storage.js +0 -164
  206. package/dist/lib/theme.js +0 -65
  207. package/dist/lib/time.js +0 -159
  208. package/dist/lib/timeline.js +0 -95
  209. package/dist/lib/timezones.js +0 -388
  210. package/dist/lib/url.js +0 -89
  211. package/dist/lib/view.js +0 -217
  212. package/dist/middleware/auth.js +0 -52
  213. package/dist/middleware/onboarding.js +0 -41
  214. package/dist/routes/api/collections.js +0 -124
  215. package/dist/routes/api/nav-items.js +0 -104
  216. package/dist/routes/api/pages.js +0 -91
  217. package/dist/routes/api/posts.js +0 -218
  218. package/dist/routes/api/search.js +0 -48
  219. package/dist/routes/api/settings.js +0 -68
  220. package/dist/routes/api/upload.js +0 -246
  221. package/dist/routes/auth/reset.js +0 -221
  222. package/dist/routes/auth/setup.js +0 -194
  223. package/dist/routes/auth/signin.js +0 -176
  224. package/dist/routes/compose.js +0 -48
  225. package/dist/routes/dash/collections.js +0 -115
  226. package/dist/routes/dash/index.js +0 -118
  227. package/dist/routes/dash/media.js +0 -106
  228. package/dist/routes/dash/pages.js +0 -294
  229. package/dist/routes/dash/posts.js +0 -244
  230. package/dist/routes/dash/redirects.js +0 -257
  231. package/dist/routes/dash/settings.js +0 -379
  232. package/dist/routes/feed/rss.js +0 -62
  233. package/dist/routes/feed/sitemap.js +0 -49
  234. package/dist/routes/pages/archive.js +0 -62
  235. package/dist/routes/pages/collection.js +0 -34
  236. package/dist/routes/pages/collections.js +0 -28
  237. package/dist/routes/pages/featured.js +0 -36
  238. package/dist/routes/pages/home.js +0 -64
  239. package/dist/routes/pages/latest.js +0 -45
  240. package/dist/routes/pages/page.js +0 -68
  241. package/dist/routes/pages/post.js +0 -44
  242. package/dist/routes/pages/search.js +0 -54
  243. package/dist/services/collection.js +0 -109
  244. package/dist/services/index.js +0 -24
  245. package/dist/services/media.js +0 -117
  246. package/dist/services/navigation.js +0 -91
  247. package/dist/services/page.js +0 -84
  248. package/dist/services/post.js +0 -229
  249. package/dist/services/redirect.js +0 -48
  250. package/dist/services/search.js +0 -67
  251. package/dist/services/settings.js +0 -68
  252. package/dist/types/bindings.js +0 -3
  253. package/dist/types/config.js +0 -147
  254. package/dist/types/constants.js +0 -27
  255. package/dist/types/entities.js +0 -3
  256. package/dist/types/lingui-react-macro.d.js +0 -9
  257. package/dist/types/operations.js +0 -3
  258. package/dist/types/props.js +0 -3
  259. package/dist/types/sortablejs.d.js +0 -5
  260. package/dist/types/views.js +0 -5
  261. package/dist/types.js +0 -11
  262. package/dist/ui/color-themes.js +0 -268
  263. package/dist/ui/compose/ComposeDialog.js +0 -467
  264. package/dist/ui/compose/ComposePrompt.js +0 -55
  265. package/dist/ui/dash/ActionButtons.js +0 -46
  266. package/dist/ui/dash/CrudPageHeader.js +0 -22
  267. package/dist/ui/dash/DangerZone.js +0 -36
  268. package/dist/ui/dash/FormatBadge.js +0 -27
  269. package/dist/ui/dash/ListItemRow.js +0 -21
  270. package/dist/ui/dash/PageForm.js +0 -195
  271. package/dist/ui/dash/PostForm.js +0 -395
  272. package/dist/ui/dash/PostList.js +0 -83
  273. package/dist/ui/dash/StatusBadge.js +0 -46
  274. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  275. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  276. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  277. package/dist/ui/dash/index.js +0 -10
  278. package/dist/ui/dash/media/MediaListContent.js +0 -166
  279. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  280. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  281. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  282. package/dist/ui/dash/settings/AccountContent.js +0 -209
  283. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  284. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  285. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  286. package/dist/ui/feed/LinkCard.js +0 -72
  287. package/dist/ui/feed/NoteCard.js +0 -58
  288. package/dist/ui/feed/QuoteCard.js +0 -63
  289. package/dist/ui/feed/ThreadPreview.js +0 -48
  290. package/dist/ui/feed/TimelineFeed.js +0 -41
  291. package/dist/ui/feed/TimelineItem.js +0 -27
  292. package/dist/ui/font-themes.js +0 -36
  293. package/dist/ui/layouts/BaseLayout.js +0 -153
  294. package/dist/ui/layouts/DashLayout.js +0 -141
  295. package/dist/ui/layouts/SiteLayout.js +0 -169
  296. package/dist/ui/pages/ArchivePage.js +0 -143
  297. package/dist/ui/pages/CollectionPage.js +0 -70
  298. package/dist/ui/pages/CollectionsPage.js +0 -76
  299. package/dist/ui/pages/FeaturedPage.js +0 -24
  300. package/dist/ui/pages/HomePage.js +0 -24
  301. package/dist/ui/pages/PostPage.js +0 -55
  302. package/dist/ui/pages/SearchPage.js +0 -122
  303. package/dist/ui/pages/SinglePage.js +0 -23
  304. package/dist/ui/shared/EmptyState.js +0 -27
  305. package/dist/ui/shared/MediaGallery.js +0 -35
  306. package/dist/ui/shared/Pagination.js +0 -195
  307. package/dist/ui/shared/ThreadView.js +0 -108
  308. package/dist/ui/shared/index.js +0 -5
  309. package/dist/vendor/datastar.js +0 -1606
  310. package/src/lib/__tests__/config.test.ts +0 -192
  311. package/src/lib/config.ts +0 -167
  312. package/src/routes/compose.ts +0 -63
  313. package/src/ui/dash/PostForm.tsx +0 -360
  314. 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
- */