@jant/core 0.3.26 → 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 (314) 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 +112 -173
  9. package/src/auth.ts +4 -1
  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 -265
  172. package/dist/auth.js +0 -36
  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
package/dist/lib/view.js DELETED
@@ -1,217 +0,0 @@
1
- /**
2
- * View Model Conversions (v2)
3
- *
4
- * Transforms raw database models into render-ready View types.
5
- * Theme components receive only View types -- no lib/ imports needed.
6
- */ import { encode } from "./sqid.js";
7
- import { toISOString, formatDate, formatTime, formatRelativeTime } from "./time.js";
8
- import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
9
- import { getHtmlExcerpt } from "./excerpt.js";
10
- /**
11
- * Creates a MediaContext from Hono context environment variables.
12
- *
13
- * @param c - Hono context
14
- * @returns MediaContext with env values
15
- */ export function createMediaContext(c) {
16
- return {
17
- r2PublicUrl: c.env.R2_PUBLIC_URL,
18
- imageTransformUrl: c.env.IMAGE_TRANSFORM_URL,
19
- s3PublicUrl: c.env.S3_PUBLIC_URL
20
- };
21
- }
22
- // =============================================================================
23
- // Media Conversions
24
- // =============================================================================
25
- /**
26
- * Converts a raw Media record to a render-ready MediaView.
27
- *
28
- * @param media - Raw media record from database
29
- * @param ctx - Media context with URL configuration
30
- * @returns Render-ready MediaView with pre-computed URLs
31
- */ export function toMediaView(media, ctx) {
32
- const publicUrl = getPublicUrlForProvider(media.provider, ctx.r2PublicUrl, ctx.s3PublicUrl);
33
- const url = getMediaUrl(media.storageKey, publicUrl);
34
- const thumbnailUrl = getImageUrl(url, ctx.imageTransformUrl, {
35
- width: 400,
36
- quality: 80,
37
- format: "auto",
38
- fit: "cover"
39
- });
40
- return {
41
- id: media.id,
42
- url,
43
- thumbnailUrl,
44
- mimeType: media.mimeType,
45
- altText: media.alt ?? undefined,
46
- width: media.width ?? undefined,
47
- height: media.height ?? undefined,
48
- size: media.size
49
- };
50
- }
51
- // =============================================================================
52
- // Post Conversions
53
- // =============================================================================
54
- /**
55
- * Converts a PostWithMedia to a render-ready PostView.
56
- *
57
- * @param post - Post with media attachments from database
58
- * @param _ctx - Media context with URL configuration
59
- * @returns Render-ready PostView with pre-computed fields
60
- */ export function toPostView(post, _ctx) {
61
- const permalink = post.path ? `/${post.path}` : `/p/${encode(post.id)}`;
62
- // Pre-compute excerpt from raw body
63
- let excerpt;
64
- if (post.body) {
65
- excerpt = post.body.length > 160 ? post.body.slice(0, 160) + "..." : post.body;
66
- }
67
- // Pre-compute HTML summary for article-style posts (with title)
68
- let summaryHtml;
69
- let summaryHasMore;
70
- if (post.title && post.bodyHtml) {
71
- const result = getHtmlExcerpt(post.bodyHtml);
72
- summaryHtml = result.excerpt;
73
- summaryHasMore = result.hasMore;
74
- }
75
- // Convert media attachments
76
- const media = post.mediaAttachments.map((m)=>({
77
- id: m.id,
78
- url: m.url,
79
- thumbnailUrl: m.previewUrl,
80
- mimeType: m.mimeType,
81
- altText: m.alt ?? undefined,
82
- width: m.width ?? undefined,
83
- height: m.height ?? undefined
84
- }));
85
- return {
86
- id: post.id,
87
- permalink,
88
- path: post.path ?? undefined,
89
- title: post.title ?? undefined,
90
- bodyHtml: post.bodyHtml ?? undefined,
91
- excerpt,
92
- summaryHtml,
93
- summaryHasMore,
94
- url: post.url ?? undefined,
95
- quoteText: post.quoteText ?? undefined,
96
- format: post.format,
97
- status: post.status,
98
- featured: post.featured === 1,
99
- pinned: post.pinned === 1,
100
- rating: post.rating ?? undefined,
101
- collectionId: post.collectionId ?? undefined,
102
- publishedAt: toISOString(post.publishedAt),
103
- publishedAtFormatted: formatDate(post.publishedAt),
104
- publishedAtTime: formatTime(post.publishedAt),
105
- publishedAtRelative: formatRelativeTime(post.publishedAt),
106
- updatedAt: toISOString(post.updatedAt),
107
- media,
108
- replyToId: post.replyToId ?? undefined,
109
- threadRootId: post.threadId ?? undefined,
110
- body: post.body ?? undefined
111
- };
112
- }
113
- /**
114
- * Batch converts PostWithMedia[] to PostView[].
115
- */ export function toPostViews(posts, ctx) {
116
- return posts.map((p)=>toPostView(p, ctx));
117
- }
118
- /**
119
- * Converts a bare Post (no media) to a PostView with empty media array.
120
- */ export function toPostViewFromPost(post, ctx) {
121
- return toPostView({
122
- ...post,
123
- mediaAttachments: []
124
- }, ctx);
125
- }
126
- /**
127
- * Batch converts Post[] (no media) to PostView[].
128
- */ export function toPostViewsFromPosts(posts, ctx) {
129
- return posts.map((p)=>toPostViewFromPost(p, ctx));
130
- }
131
- // =============================================================================
132
- // Page Conversions
133
- // =============================================================================
134
- /**
135
- * Converts a Page to a render-ready PageView.
136
- */ export function toPageView(page) {
137
- return {
138
- id: page.id,
139
- slug: page.slug,
140
- title: page.title ?? undefined,
141
- bodyHtml: page.bodyHtml ?? undefined,
142
- status: page.status,
143
- createdAt: toISOString(page.createdAt),
144
- updatedAt: toISOString(page.updatedAt)
145
- };
146
- }
147
- // =============================================================================
148
- // Navigation Conversions
149
- // =============================================================================
150
- /**
151
- * Converts a NavItem to a NavItemView with pre-computed state.
152
- */ export function toNavItemView(item, currentPath) {
153
- const isExternal = item.url.startsWith("http://") || item.url.startsWith("https://");
154
- let isActive = false;
155
- if (!isExternal) {
156
- if (item.url === "/") {
157
- isActive = currentPath === "/";
158
- } else {
159
- isActive = currentPath === item.url || currentPath.startsWith(item.url + "/");
160
- }
161
- }
162
- return {
163
- id: item.id,
164
- type: item.type,
165
- label: item.label,
166
- url: item.url,
167
- pageId: item.pageId ?? undefined,
168
- isActive,
169
- isExternal
170
- };
171
- }
172
- /**
173
- * Batch converts NavItem[] to NavItemView[].
174
- */ export function toNavItemViews(items, currentPath) {
175
- return items.map((item)=>toNavItemView(item, currentPath));
176
- }
177
- // =============================================================================
178
- // Search Result Conversions
179
- // =============================================================================
180
- /**
181
- * Converts a SearchResult to a SearchResultView with PostView.
182
- */ export function toSearchResultView(result, ctx) {
183
- return {
184
- post: toPostViewFromPost(result.post, ctx),
185
- rank: result.rank,
186
- snippet: result.snippet
187
- };
188
- }
189
- /**
190
- * Batch converts SearchResult[] to SearchResultView[].
191
- */ export function toSearchResultViews(results, ctx) {
192
- return results.map((r)=>toSearchResultView(r, ctx));
193
- }
194
- // =============================================================================
195
- // Archive Group Conversions
196
- // =============================================================================
197
- /**
198
- * Converts a grouped post map to typed ArchiveGroup[].
199
- */ export function toArchiveGroups(grouped, ctx) {
200
- const groups = [];
201
- for (const [yearMonth, posts] of grouped){
202
- const [year, month] = yearMonth.split("-");
203
- if (!year || !month) continue;
204
- const date = new Date(parseInt(year, 10), parseInt(month, 10) - 1);
205
- const label = date.toLocaleDateString("en-US", {
206
- year: "numeric",
207
- month: "long"
208
- });
209
- groups.push({
210
- year,
211
- month,
212
- label,
213
- posts: toPostViewsFromPosts(posts, ctx)
214
- });
215
- }
216
- return groups;
217
- }
@@ -1,52 +0,0 @@
1
- /**
2
- * Authentication Middleware
3
- *
4
- * Protects routes by requiring authentication
5
- */ /**
6
- * Middleware that requires authentication.
7
- * Redirects to signin page if not authenticated.
8
- */ export function requireAuth(redirectTo = "/signin") {
9
- return async (c, next)=>{
10
- if (!c.var.auth) {
11
- return c.redirect(redirectTo);
12
- }
13
- try {
14
- const session = await c.var.auth.api.getSession({
15
- headers: c.req.raw.headers
16
- });
17
- if (!session?.user) {
18
- return c.redirect(redirectTo);
19
- }
20
- await next();
21
- } catch {
22
- return c.redirect(redirectTo);
23
- }
24
- };
25
- }
26
- /**
27
- * Middleware for API routes that requires authentication.
28
- * Returns 401 if not authenticated.
29
- */ export function requireAuthApi() {
30
- return async (c, next)=>{
31
- if (!c.var.auth) {
32
- return c.json({
33
- error: "Authentication not configured"
34
- }, 500);
35
- }
36
- try {
37
- const session = await c.var.auth.api.getSession({
38
- headers: c.req.raw.headers
39
- });
40
- if (!session?.user) {
41
- return c.json({
42
- error: "Unauthorized"
43
- }, 401);
44
- }
45
- await next();
46
- } catch {
47
- return c.json({
48
- error: "Unauthorized"
49
- }, 401);
50
- }
51
- };
52
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * Onboarding Middleware
3
- *
4
- * Redirects key page routes to /setup if onboarding hasn't been completed.
5
- * Uses an allowlist approach: only explicitly listed page routes are redirected,
6
- * so static assets, API endpoints, feeds, and other resources always pass through.
7
- * Caches the result in memory so the DB is only queried once per isolate lifetime.
8
- */ /** In-memory cache — persists across requests within a Worker isolate */ let onboardingComplete = false;
9
- /**
10
- * Middleware that redirects to /setup if onboarding is not complete.
11
- * Uses module-level caching: once onboarding is confirmed complete,
12
- * no further DB queries are made for the lifetime of the Worker isolate.
13
- */ export function requireOnboarding() {
14
- return async (c, next)=>{
15
- if (onboardingComplete) {
16
- return next();
17
- }
18
- const path = new URL(c.req.url).pathname;
19
- if (!shouldRedirect(path)) {
20
- return next();
21
- }
22
- const isComplete = await c.var.services.settings.isOnboardingComplete();
23
- if (isComplete) {
24
- onboardingComplete = true;
25
- return next();
26
- }
27
- return c.redirect("/setup");
28
- };
29
- }
30
- /**
31
- * Only these page routes are redirected to /setup during onboarding.
32
- * Everything else (assets, API, feeds, media, etc.) passes through.
33
- */ function shouldRedirect(path) {
34
- return path === "/" || path === "/signin" || path === "/reset" || path.startsWith("/dash");
35
- }
36
- /**
37
- * Reset the onboarding cache. Only for testing.
38
- * @internal
39
- */ export function resetOnboardingCache() {
40
- onboardingComplete = false;
41
- }
@@ -1,124 +0,0 @@
1
- /**
2
- * Collections API Routes
3
- */ import { Hono } from "hono";
4
- import { requireAuthApi } from "../../middleware/auth.js";
5
- import { z } from "zod";
6
- import { SORT_ORDERS } from "../../types.js";
7
- export const collectionsApiRoutes = new Hono();
8
- const SortOrderSchema = z.enum(SORT_ORDERS);
9
- const CreateCollectionSchema = z.object({
10
- slug: z.string().min(1),
11
- title: z.string().min(1),
12
- description: z.string().optional(),
13
- icon: z.string().optional(),
14
- sortOrder: SortOrderSchema.optional(),
15
- position: z.number().int().min(0).optional(),
16
- showDivider: z.boolean().optional()
17
- });
18
- const UpdateCollectionSchema = z.object({
19
- slug: z.string().min(1).optional(),
20
- title: z.string().min(1).optional(),
21
- description: z.string().nullable().optional(),
22
- icon: z.string().nullable().optional(),
23
- sortOrder: SortOrderSchema.optional(),
24
- position: z.number().int().min(0).optional(),
25
- showDivider: z.boolean().optional()
26
- });
27
- const ReorderSchema = z.object({
28
- ids: z.array(z.number().int().positive())
29
- });
30
- // List collections (includes post counts)
31
- collectionsApiRoutes.get("/", async (c)=>{
32
- const collections = await c.var.services.collections.list();
33
- const postCounts = await c.var.services.collections.getPostCounts();
34
- return c.json({
35
- collections: collections.map((col)=>({
36
- ...col,
37
- postCount: postCounts.get(col.id) ?? 0
38
- }))
39
- });
40
- });
41
- // Get single collection
42
- collectionsApiRoutes.get("/:id", async (c)=>{
43
- const id = parseInt(c.req.param("id"), 10);
44
- if (isNaN(id)) return c.json({
45
- error: "Invalid ID"
46
- }, 400);
47
- const collection = await c.var.services.collections.getById(id);
48
- if (!collection) return c.json({
49
- error: "Not found"
50
- }, 404);
51
- return c.json(collection);
52
- });
53
- // Reorder collections (requires auth) — must be before /:id
54
- collectionsApiRoutes.put("/reorder", requireAuthApi(), async (c)=>{
55
- const rawBody = await c.req.json();
56
- const parseResult = ReorderSchema.safeParse(rawBody);
57
- if (!parseResult.success) {
58
- return c.json({
59
- error: "Validation failed",
60
- details: parseResult.error.flatten()
61
- }, 400);
62
- }
63
- await c.var.services.collections.reorder(parseResult.data.ids);
64
- const collections = await c.var.services.collections.list();
65
- return c.json({
66
- collections
67
- });
68
- });
69
- // Create collection (requires auth)
70
- collectionsApiRoutes.post("/", requireAuthApi(), async (c)=>{
71
- const rawBody = await c.req.json();
72
- const parseResult = CreateCollectionSchema.safeParse(rawBody);
73
- if (!parseResult.success) {
74
- return c.json({
75
- error: "Validation failed",
76
- details: parseResult.error.flatten()
77
- }, 400);
78
- }
79
- const body = parseResult.data;
80
- const collection = await c.var.services.collections.create({
81
- slug: body.slug,
82
- title: body.title,
83
- description: body.description,
84
- icon: body.icon,
85
- sortOrder: body.sortOrder,
86
- position: body.position,
87
- showDivider: body.showDivider
88
- });
89
- return c.json(collection, 201);
90
- });
91
- // Update collection (requires auth)
92
- collectionsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
93
- const id = parseInt(c.req.param("id"), 10);
94
- if (isNaN(id)) return c.json({
95
- error: "Invalid ID"
96
- }, 400);
97
- const rawBody = await c.req.json();
98
- const parseResult = UpdateCollectionSchema.safeParse(rawBody);
99
- if (!parseResult.success) {
100
- return c.json({
101
- error: "Validation failed",
102
- details: parseResult.error.flatten()
103
- }, 400);
104
- }
105
- const collection = await c.var.services.collections.update(id, parseResult.data);
106
- if (!collection) return c.json({
107
- error: "Not found"
108
- }, 404);
109
- return c.json(collection);
110
- });
111
- // Delete collection (requires auth)
112
- collectionsApiRoutes.delete("/:id", requireAuthApi(), async (c)=>{
113
- const id = parseInt(c.req.param("id"), 10);
114
- if (isNaN(id)) return c.json({
115
- error: "Invalid ID"
116
- }, 400);
117
- const success = await c.var.services.collections.delete(id);
118
- if (!success) return c.json({
119
- error: "Not found"
120
- }, 404);
121
- return c.json({
122
- success: true
123
- });
124
- });
@@ -1,104 +0,0 @@
1
- /**
2
- * Nav Items API Routes
3
- */ import { Hono } from "hono";
4
- import { requireAuthApi } from "../../middleware/auth.js";
5
- import { z } from "zod";
6
- export const navItemsApiRoutes = new Hono();
7
- const NavItemTypeSchema = z.enum([
8
- "link",
9
- "page"
10
- ]);
11
- const CreateNavItemSchema = z.object({
12
- type: NavItemTypeSchema,
13
- label: z.string().min(1),
14
- url: z.string().min(1),
15
- pageId: z.number().int().positive().optional(),
16
- position: z.number().int().min(0).optional()
17
- });
18
- const UpdateNavItemSchema = z.object({
19
- type: NavItemTypeSchema.optional(),
20
- label: z.string().min(1).optional(),
21
- url: z.string().min(1).optional(),
22
- pageId: z.number().int().positive().nullable().optional(),
23
- position: z.number().int().min(0).optional()
24
- });
25
- const ReorderSchema = z.object({
26
- ids: z.array(z.number().int().positive())
27
- });
28
- // List nav items
29
- navItemsApiRoutes.get("/", async (c)=>{
30
- const items = await c.var.services.navItems.list();
31
- return c.json({
32
- navItems: items
33
- });
34
- });
35
- // Reorder nav items (requires auth) — must be before /:id
36
- navItemsApiRoutes.put("/reorder", requireAuthApi(), async (c)=>{
37
- const rawBody = await c.req.json();
38
- const parseResult = ReorderSchema.safeParse(rawBody);
39
- if (!parseResult.success) {
40
- return c.json({
41
- error: "Validation failed",
42
- details: parseResult.error.flatten()
43
- }, 400);
44
- }
45
- await c.var.services.navItems.reorder(parseResult.data.ids);
46
- const items = await c.var.services.navItems.list();
47
- return c.json({
48
- navItems: items
49
- });
50
- });
51
- // Create nav item (requires auth)
52
- navItemsApiRoutes.post("/", requireAuthApi(), async (c)=>{
53
- const rawBody = await c.req.json();
54
- const parseResult = CreateNavItemSchema.safeParse(rawBody);
55
- if (!parseResult.success) {
56
- return c.json({
57
- error: "Validation failed",
58
- details: parseResult.error.flatten()
59
- }, 400);
60
- }
61
- const body = parseResult.data;
62
- const item = await c.var.services.navItems.create({
63
- type: body.type,
64
- label: body.label,
65
- url: body.url,
66
- pageId: body.pageId,
67
- position: body.position
68
- });
69
- return c.json(item, 201);
70
- });
71
- // Update nav item (requires auth)
72
- navItemsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
73
- const id = parseInt(c.req.param("id"), 10);
74
- if (isNaN(id)) return c.json({
75
- error: "Invalid ID"
76
- }, 400);
77
- const rawBody = await c.req.json();
78
- const parseResult = UpdateNavItemSchema.safeParse(rawBody);
79
- if (!parseResult.success) {
80
- return c.json({
81
- error: "Validation failed",
82
- details: parseResult.error.flatten()
83
- }, 400);
84
- }
85
- const item = await c.var.services.navItems.update(id, parseResult.data);
86
- if (!item) return c.json({
87
- error: "Not found"
88
- }, 404);
89
- return c.json(item);
90
- });
91
- // Delete nav item (requires auth)
92
- navItemsApiRoutes.delete("/:id", requireAuthApi(), async (c)=>{
93
- const id = parseInt(c.req.param("id"), 10);
94
- if (isNaN(id)) return c.json({
95
- error: "Invalid ID"
96
- }, 400);
97
- const success = await c.var.services.navItems.delete(id);
98
- if (!success) return c.json({
99
- error: "Not found"
100
- }, 404);
101
- return c.json({
102
- success: true
103
- });
104
- });
@@ -1,91 +0,0 @@
1
- /**
2
- * Pages API Routes
3
- */ import { Hono } from "hono";
4
- import { requireAuthApi } from "../../middleware/auth.js";
5
- import { z } from "zod";
6
- import { StatusSchema } from "../../lib/schemas.js";
7
- export const pagesApiRoutes = new Hono();
8
- const CreatePageSchema = z.object({
9
- slug: z.string().min(1),
10
- title: z.string().optional(),
11
- body: z.string().optional(),
12
- status: StatusSchema.optional()
13
- });
14
- const UpdatePageSchema = z.object({
15
- slug: z.string().min(1).optional(),
16
- title: z.string().nullable().optional(),
17
- body: z.string().nullable().optional(),
18
- status: StatusSchema.optional()
19
- });
20
- // List pages
21
- pagesApiRoutes.get("/", async (c)=>{
22
- const pages = await c.var.services.pages.list();
23
- return c.json({
24
- pages
25
- });
26
- });
27
- // Get single page
28
- pagesApiRoutes.get("/:id", async (c)=>{
29
- const id = parseInt(c.req.param("id"), 10);
30
- if (isNaN(id)) return c.json({
31
- error: "Invalid ID"
32
- }, 400);
33
- const page = await c.var.services.pages.getById(id);
34
- if (!page) return c.json({
35
- error: "Not found"
36
- }, 404);
37
- return c.json(page);
38
- });
39
- // Create page (requires auth)
40
- pagesApiRoutes.post("/", requireAuthApi(), async (c)=>{
41
- const rawBody = await c.req.json();
42
- const parseResult = CreatePageSchema.safeParse(rawBody);
43
- if (!parseResult.success) {
44
- return c.json({
45
- error: "Validation failed",
46
- details: parseResult.error.flatten()
47
- }, 400);
48
- }
49
- const body = parseResult.data;
50
- const page = await c.var.services.pages.create({
51
- slug: body.slug,
52
- title: body.title,
53
- body: body.body,
54
- status: body.status
55
- });
56
- return c.json(page, 201);
57
- });
58
- // Update page (requires auth)
59
- pagesApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
60
- const id = parseInt(c.req.param("id"), 10);
61
- if (isNaN(id)) return c.json({
62
- error: "Invalid ID"
63
- }, 400);
64
- const rawBody = await c.req.json();
65
- const parseResult = UpdatePageSchema.safeParse(rawBody);
66
- if (!parseResult.success) {
67
- return c.json({
68
- error: "Validation failed",
69
- details: parseResult.error.flatten()
70
- }, 400);
71
- }
72
- const page = await c.var.services.pages.update(id, parseResult.data);
73
- if (!page) return c.json({
74
- error: "Not found"
75
- }, 404);
76
- return c.json(page);
77
- });
78
- // Delete page (requires auth)
79
- pagesApiRoutes.delete("/:id", requireAuthApi(), async (c)=>{
80
- const id = parseInt(c.req.param("id"), 10);
81
- if (isNaN(id)) return c.json({
82
- error: "Invalid ID"
83
- }, 400);
84
- const success = await c.var.services.pages.delete(id);
85
- if (!success) return c.json({
86
- error: "Not found"
87
- }, 404);
88
- return c.json({
89
- success: true
90
- });
91
- });