@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
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
- });