@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
@@ -1,64 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Home Page Route
4
- *
5
- * Timeline feed with per-type card components and thread previews.
6
- * Uses page-based pagination.
7
- *
8
- * When HOME_DEFAULT_VIEW is "featured", the homepage shows featured posts
9
- * instead of latest. The /latest route always shows latest posts explicitly.
10
- */ import { Hono } from "hono";
11
- import { getNavigationData } from "../../lib/navigation.js";
12
- import { renderPublicPage } from "../../lib/render.js";
13
- import { assembleTimeline } from "../../lib/timeline.js";
14
- import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
15
- import { HomePage } from "../../ui/pages/HomePage.js";
16
- import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
17
- export const homeRoutes = new Hono();
18
- homeRoutes.get("/", async (c)=>{
19
- const navData = await getNavigationData(c);
20
- if (navData.homeDefaultView === "featured") {
21
- // Show featured posts on homepage
22
- const posts = await c.var.services.posts.list({
23
- featured: true,
24
- status: "published",
25
- excludeReplies: true
26
- });
27
- const mediaCtx = createMediaContext(c);
28
- const postViews = toPostViewsFromPosts(posts, mediaCtx);
29
- const items = postViews.map((post)=>({
30
- post
31
- }));
32
- return renderPublicPage(c, {
33
- title: navData.siteName,
34
- navData,
35
- content: /*#__PURE__*/ _jsx(FeaturedPage, {
36
- items: items
37
- })
38
- });
39
- }
40
- // Default: show latest posts
41
- const pageParam = c.req.query("page");
42
- const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
43
- const { items, currentPage, totalPages } = await assembleTimeline(c, {
44
- page
45
- });
46
- // Fetch pinned posts
47
- const pinnedPosts = await c.var.services.posts.list({
48
- pinned: true,
49
- status: "published",
50
- excludeReplies: true
51
- });
52
- const mediaCtx = createMediaContext(c);
53
- const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
54
- return renderPublicPage(c, {
55
- title: navData.siteName,
56
- navData,
57
- content: /*#__PURE__*/ _jsx(HomePage, {
58
- items: items,
59
- pinnedItems: pinnedItems,
60
- currentPage: currentPage,
61
- totalPages: totalPages
62
- })
63
- });
64
- });
@@ -1,45 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Latest Page Route
4
- *
5
- * Explicit /latest URL that always shows the latest posts timeline.
6
- * When HOME_DEFAULT_VIEW is "latest" (default), this redirects to /
7
- * to avoid duplicate content. When it's "featured", this serves as
8
- * the explicit latest view.
9
- */ import { Hono } from "hono";
10
- import { getNavigationData } from "../../lib/navigation.js";
11
- import { renderPublicPage } from "../../lib/render.js";
12
- import { assembleTimeline } from "../../lib/timeline.js";
13
- import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
14
- import { HomePage } from "../../ui/pages/HomePage.js";
15
- export const latestRoutes = new Hono();
16
- latestRoutes.get("/", async (c)=>{
17
- const navData = await getNavigationData(c);
18
- // When homepage already shows latest, redirect to avoid duplicate content
19
- if (navData.homeDefaultView !== "featured") {
20
- return c.redirect("/", 302);
21
- }
22
- const pageParam = c.req.query("page");
23
- const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
24
- const { items, currentPage, totalPages } = await assembleTimeline(c, {
25
- page
26
- });
27
- // Fetch pinned posts
28
- const pinnedPosts = await c.var.services.posts.list({
29
- pinned: true,
30
- status: "published",
31
- excludeReplies: true
32
- });
33
- const mediaCtx = createMediaContext(c);
34
- const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
35
- return renderPublicPage(c, {
36
- title: `Latest - ${navData.siteName}`,
37
- navData,
38
- content: /*#__PURE__*/ _jsx(HomePage, {
39
- items: items,
40
- pinnedItems: pinnedItems,
41
- currentPage: currentPage,
42
- totalPages: totalPages
43
- })
44
- });
45
- });
@@ -1,68 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Custom Page Route
4
- *
5
- * Serves pages from the pages table and posts with custom paths.
6
- * This is a catch-all route mounted at "/" - must be registered last.
7
- * Supports multi-level paths (e.g. /2024/my-post) for posts.
8
- */ import { Hono } from "hono";
9
- import { SinglePage } from "../../ui/pages/SinglePage.js";
10
- import { PostPage } from "../../ui/pages/PostPage.js";
11
- import { getNavigationData } from "../../lib/navigation.js";
12
- import { renderPublicPage } from "../../lib/render.js";
13
- import { buildMediaMap } from "../../lib/media-helpers.js";
14
- import { createMediaContext, toPageView, toPostView } from "../../lib/view.js";
15
- export const pageRoutes = new Hono();
16
- // Catch-all for custom page slugs and post paths (including multi-level)
17
- pageRoutes.get("/*", async (c)=>{
18
- const fullPath = c.req.path.slice(1); // Remove leading /
19
- if (!fullPath) return c.notFound();
20
- const isMultiSegment = fullPath.includes("/");
21
- // Pages only have single-level slugs; skip page lookup for multi-segment paths
22
- if (!isMultiSegment) {
23
- const page = await c.var.services.pages.getBySlug(fullPath);
24
- if (page) {
25
- if (page.status === "draft") {
26
- return c.notFound();
27
- }
28
- const navData = await getNavigationData(c);
29
- const pageView = toPageView(page);
30
- return renderPublicPage(c, {
31
- title: `${page.title || fullPath} - ${navData.siteName}`,
32
- description: page.body?.slice(0, 160),
33
- navData,
34
- content: /*#__PURE__*/ _jsx(SinglePage, {
35
- page: pageView
36
- })
37
- });
38
- }
39
- }
40
- // Posts support multi-level paths
41
- const post = await c.var.services.posts.getByPath(fullPath);
42
- if (post) {
43
- if (post.status === "draft") {
44
- return c.notFound();
45
- }
46
- // Load media attachments
47
- const rawMediaMap = await c.var.services.media.getByPostIds([
48
- post.id
49
- ]);
50
- const mediaCtx = createMediaContext(c);
51
- const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
52
- const postView = toPostView({
53
- ...post,
54
- mediaAttachments: mediaMap.get(post.id) ?? []
55
- }, mediaCtx);
56
- const navData = await getNavigationData(c);
57
- const title = post.title || navData.siteName;
58
- return renderPublicPage(c, {
59
- title,
60
- description: post.body?.slice(0, 160),
61
- navData,
62
- content: /*#__PURE__*/ _jsx(PostPage, {
63
- post: postView
64
- })
65
- });
66
- }
67
- return c.notFound();
68
- });
@@ -1,44 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Single Post Page Route
4
- */ import { Hono } from "hono";
5
- import { PostPage } from "../../ui/pages/PostPage.js";
6
- import * as sqid from "../../lib/sqid.js";
7
- import { getNavigationData } from "../../lib/navigation.js";
8
- import { renderPublicPage } from "../../lib/render.js";
9
- import { buildMediaMap } from "../../lib/media-helpers.js";
10
- import { createMediaContext, toPostView } from "../../lib/view.js";
11
- export const postRoutes = new Hono();
12
- postRoutes.get("/:id", async (c)=>{
13
- const paramId = c.req.param("id");
14
- // Decode sqid to numeric ID
15
- const id = sqid.decode(paramId);
16
- if (!id) return c.notFound();
17
- const post = await c.var.services.posts.getById(id);
18
- if (!post) return c.notFound();
19
- // Don't show drafts on public site
20
- if (post.status === "draft") {
21
- return c.notFound();
22
- }
23
- // Batch load media attachments
24
- const rawMediaMap = await c.var.services.media.getByPostIds([
25
- post.id
26
- ]);
27
- const mediaCtx = createMediaContext(c);
28
- const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
29
- // Transform to View Model
30
- const postView = toPostView({
31
- ...post,
32
- mediaAttachments: mediaMap.get(post.id) ?? []
33
- }, mediaCtx);
34
- const navData = await getNavigationData(c);
35
- const title = post.title || navData.siteName;
36
- return renderPublicPage(c, {
37
- title,
38
- description: post.body?.slice(0, 160),
39
- navData,
40
- content: /*#__PURE__*/ _jsx(PostPage, {
41
- post: postView
42
- })
43
- });
44
- });
@@ -1,54 +0,0 @@
1
- import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
- /**
3
- * Search Page Route
4
- */ import { Hono } from "hono";
5
- import { SearchPage } from "../../ui/pages/SearchPage.js";
6
- import { getNavigationData } from "../../lib/navigation.js";
7
- import { renderPublicPage } from "../../lib/render.js";
8
- import { createMediaContext, toSearchResultViews } from "../../lib/view.js";
9
- const PAGE_SIZE = 10;
10
- export const searchRoutes = new Hono();
11
- searchRoutes.get("/", async (c)=>{
12
- const query = c.req.query("q") || "";
13
- const pageParam = c.req.query("page");
14
- const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
15
- const navData = await getNavigationData(c);
16
- // Only search if there's a query
17
- let results = [];
18
- let error;
19
- let hasMore = false;
20
- if (query.trim()) {
21
- try {
22
- // Fetch one extra to check for more
23
- results = await c.var.services.search.search(query, {
24
- limit: PAGE_SIZE + 1,
25
- offset: (page - 1) * PAGE_SIZE,
26
- status: [
27
- "published"
28
- ]
29
- });
30
- hasMore = results.length > PAGE_SIZE;
31
- if (hasMore) {
32
- results = results.slice(0, PAGE_SIZE);
33
- }
34
- } catch (err) {
35
- // eslint-disable-next-line no-console -- Error logging is intentional
36
- console.error("Search error:", err);
37
- error = "Search failed. Please try again.";
38
- }
39
- }
40
- // Transform to View Models
41
- const mediaCtx = createMediaContext(c);
42
- const resultViews = toSearchResultViews(results, mediaCtx);
43
- return renderPublicPage(c, {
44
- title: query ? `Search: ${query} - ${navData.siteName}` : `Search - ${navData.siteName}`,
45
- navData,
46
- content: /*#__PURE__*/ _jsx(SearchPage, {
47
- query: query,
48
- results: resultViews,
49
- error: error,
50
- hasMore: hasMore,
51
- page: page
52
- })
53
- });
54
- });
@@ -1,109 +0,0 @@
1
- /**
2
- * Collection Service (v2)
3
- *
4
- * Manages collections. Posts belong to collections via posts.collection_id (1:M).
5
- */ import { eq, asc, sql, desc } from "drizzle-orm";
6
- import { collections, posts } from "../db/schema.js";
7
- import { now } from "../lib/time.js";
8
- export function createCollectionService(db) {
9
- function toCollection(row) {
10
- return {
11
- id: row.id,
12
- slug: row.slug,
13
- title: row.title,
14
- description: row.description,
15
- icon: row.icon,
16
- sortOrder: row.sortOrder,
17
- position: row.position,
18
- showDivider: row.showDivider,
19
- createdAt: row.createdAt,
20
- updatedAt: row.updatedAt
21
- };
22
- }
23
- return {
24
- async getById (id) {
25
- const result = await db.select().from(collections).where(eq(collections.id, id)).limit(1);
26
- return result[0] ? toCollection(result[0]) : null;
27
- },
28
- async getBySlug (slug) {
29
- const result = await db.select().from(collections).where(eq(collections.slug, slug)).limit(1);
30
- return result[0] ? toCollection(result[0]) : null;
31
- },
32
- async list () {
33
- const rows = await db.select().from(collections).orderBy(asc(collections.position), desc(collections.createdAt));
34
- return rows.map(toCollection);
35
- },
36
- async create (data) {
37
- const timestamp = now();
38
- let position = data.position;
39
- if (position === undefined) {
40
- const maxResult = await db.select({
41
- maxPos: sql`COALESCE(MAX(position), -1)`
42
- }).from(collections);
43
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- aggregate always returns one row
44
- position = maxResult[0].maxPos + 1;
45
- }
46
- const result = await db.insert(collections).values({
47
- slug: data.slug,
48
- title: data.title,
49
- description: data.description ?? null,
50
- icon: data.icon ?? null,
51
- sortOrder: data.sortOrder ?? "newest",
52
- position,
53
- showDivider: data.showDivider ? 1 : 0,
54
- createdAt: timestamp,
55
- updatedAt: timestamp
56
- }).returning();
57
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
58
- return toCollection(result[0]);
59
- },
60
- async update (id, data) {
61
- const existing = await this.getById(id);
62
- if (!existing) return null;
63
- const timestamp = now();
64
- const updates = {
65
- updatedAt: timestamp
66
- };
67
- if (data.title !== undefined) updates.title = data.title;
68
- if (data.slug !== undefined) updates.slug = data.slug;
69
- if (data.description !== undefined) updates.description = data.description;
70
- if (data.icon !== undefined) updates.icon = data.icon;
71
- if (data.sortOrder !== undefined) updates.sortOrder = data.sortOrder;
72
- if (data.position !== undefined) updates.position = data.position;
73
- if (data.showDivider !== undefined) updates.showDivider = data.showDivider ? 1 : 0;
74
- const result = await db.update(collections).set(updates).where(eq(collections.id, id)).returning();
75
- return result[0] ? toCollection(result[0]) : null;
76
- },
77
- async delete (id) {
78
- // Clear collection_id on posts that belong to this collection
79
- await db.update(posts).set({
80
- collectionId: null
81
- }).where(eq(posts.collectionId, id));
82
- const result = await db.delete(collections).where(eq(collections.id, id)).returning();
83
- return result.length > 0;
84
- },
85
- async reorder (ids) {
86
- const timestamp = now();
87
- for(let i = 0; i < ids.length; i++){
88
- await db.update(collections).set({
89
- position: i,
90
- updatedAt: timestamp
91
- })// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- loop index guarantees element exists
92
- .where(eq(collections.id, ids[i]));
93
- }
94
- },
95
- async getPostCounts () {
96
- const rows = await db.select({
97
- collectionId: posts.collectionId,
98
- count: sql`count(*)`.as("count")
99
- }).from(posts).where(sql`${posts.collectionId} IS NOT NULL AND ${posts.deletedAt} IS NULL`).groupBy(posts.collectionId);
100
- const counts = new Map();
101
- for (const row of rows){
102
- if (row.collectionId !== null) {
103
- counts.set(row.collectionId, row.count);
104
- }
105
- }
106
- return counts;
107
- }
108
- };
109
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * Services (v2)
3
- *
4
- * Business logic layer
5
- */ import { createSettingsService } from "./settings.js";
6
- import { createPostService } from "./post.js";
7
- import { createPageService } from "./page.js";
8
- import { createRedirectService } from "./redirect.js";
9
- import { createMediaService } from "./media.js";
10
- import { createCollectionService } from "./collection.js";
11
- import { createSearchService } from "./search.js";
12
- import { createNavItemService } from "./navigation.js";
13
- export function createServices(db, d1) {
14
- return {
15
- settings: createSettingsService(db),
16
- posts: createPostService(db),
17
- pages: createPageService(db),
18
- redirects: createRedirectService(db),
19
- media: createMediaService(db),
20
- collections: createCollectionService(db),
21
- search: createSearchService(d1),
22
- navItems: createNavItemService(db)
23
- };
24
- }
@@ -1,117 +0,0 @@
1
- /**
2
- * Media Service
3
- *
4
- * Handles media upload and management with pluggable storage backends.
5
- */ import { eq, desc, inArray, asc } from "drizzle-orm";
6
- import { uuidv7 } from "uuidv7";
7
- import { media } from "../db/schema.js";
8
- import { now } from "../lib/time.js";
9
- export function createMediaService(db) {
10
- function toMedia(row) {
11
- return {
12
- id: row.id,
13
- postId: row.postId,
14
- filename: row.filename,
15
- originalName: row.originalName,
16
- mimeType: row.mimeType,
17
- size: row.size,
18
- storageKey: row.storageKey,
19
- provider: row.provider,
20
- width: row.width,
21
- height: row.height,
22
- alt: row.alt,
23
- position: row.position,
24
- blurhash: row.blurhash,
25
- createdAt: row.createdAt
26
- };
27
- }
28
- return {
29
- async getById (id) {
30
- const result = await db.select().from(media).where(eq(media.id, id)).limit(1);
31
- return result[0] ? toMedia(result[0]) : null;
32
- },
33
- async getByIds (ids) {
34
- if (ids.length === 0) return [];
35
- const rows = await db.select().from(media).where(inArray(media.id, ids));
36
- return rows.map(toMedia);
37
- },
38
- async getByPostId (postId) {
39
- const rows = await db.select().from(media).where(eq(media.postId, postId)).orderBy(asc(media.position));
40
- return rows.map(toMedia);
41
- },
42
- async getByPostIds (postIds) {
43
- const result = new Map();
44
- if (postIds.length === 0) return result;
45
- const rows = await db.select().from(media).where(inArray(media.postId, postIds)).orderBy(asc(media.position));
46
- for (const row of rows){
47
- const m = toMedia(row);
48
- if (m.postId === null) continue;
49
- const list = result.get(m.postId);
50
- if (list) {
51
- list.push(m);
52
- } else {
53
- result.set(m.postId, [
54
- m
55
- ]);
56
- }
57
- }
58
- return result;
59
- },
60
- async getByStorageKey (storageKey) {
61
- const result = await db.select().from(media).where(eq(media.storageKey, storageKey)).limit(1);
62
- return result[0] ? toMedia(result[0]) : null;
63
- },
64
- async list (limit = 100) {
65
- const rows = await db.select().from(media).orderBy(desc(media.createdAt)).limit(limit);
66
- return rows.map(toMedia);
67
- },
68
- async create (data) {
69
- const id = data.id ?? uuidv7();
70
- const timestamp = now();
71
- const result = await db.insert(media).values({
72
- id,
73
- postId: data.postId ?? null,
74
- filename: data.filename,
75
- originalName: data.originalName,
76
- mimeType: data.mimeType,
77
- size: data.size,
78
- storageKey: data.storageKey,
79
- provider: data.provider ?? "r2",
80
- width: data.width ?? null,
81
- height: data.height ?? null,
82
- alt: data.alt ?? null,
83
- position: data.position ?? 0,
84
- blurhash: data.blurhash ?? null,
85
- createdAt: timestamp
86
- }).returning();
87
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
88
- return toMedia(result[0]);
89
- },
90
- async attachToPost (postId, mediaIds) {
91
- // Clear existing attachments
92
- await db.update(media).set({
93
- postId: null,
94
- position: 0
95
- }).where(eq(media.postId, postId));
96
- // Set new attachments with position = array index
97
- for(let i = 0; i < mediaIds.length; i++){
98
- const mediaId = mediaIds[i];
99
- if (!mediaId) continue;
100
- await db.update(media).set({
101
- postId,
102
- position: i
103
- }).where(eq(media.id, mediaId));
104
- }
105
- },
106
- async detachFromPost (postId) {
107
- await db.update(media).set({
108
- postId: null,
109
- position: 0
110
- }).where(eq(media.postId, postId));
111
- },
112
- async delete (id) {
113
- const result = await db.delete(media).where(eq(media.id, id)).returning();
114
- return result.length > 0;
115
- }
116
- };
117
- }
@@ -1,91 +0,0 @@
1
- /**
2
- * Nav Item Service (v2)
3
- *
4
- * Manages navigation items (page links and external links)
5
- */ import { eq, asc, sql } from "drizzle-orm";
6
- import { navItems } from "../db/schema.js";
7
- import { now } from "../lib/time.js";
8
- export function createNavItemService(db) {
9
- function toNavItem(row) {
10
- return {
11
- id: row.id,
12
- type: row.type,
13
- label: row.label,
14
- url: row.url,
15
- pageId: row.pageId,
16
- position: row.position,
17
- createdAt: row.createdAt,
18
- updatedAt: row.updatedAt
19
- };
20
- }
21
- return {
22
- async list () {
23
- const rows = await db.select().from(navItems).orderBy(asc(navItems.position));
24
- return rows.map(toNavItem);
25
- },
26
- async getById (id) {
27
- const result = await db.select().from(navItems).where(eq(navItems.id, id)).limit(1);
28
- return result[0] ? toNavItem(result[0]) : null;
29
- },
30
- async create (data) {
31
- const timestamp = now();
32
- let position = data.position;
33
- if (position === undefined) {
34
- const maxResult = await db.select({
35
- maxPos: sql`COALESCE(MAX(position), -1)`
36
- }).from(navItems);
37
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- aggregate always returns one row
38
- position = maxResult[0].maxPos + 1;
39
- }
40
- const result = await db.insert(navItems).values({
41
- type: data.type,
42
- label: data.label,
43
- url: data.url,
44
- pageId: data.pageId ?? null,
45
- position,
46
- createdAt: timestamp,
47
- updatedAt: timestamp
48
- }).returning();
49
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
50
- return toNavItem(result[0]);
51
- },
52
- async update (id, data) {
53
- const existing = await db.select().from(navItems).where(eq(navItems.id, id)).limit(1);
54
- if (!existing[0]) return null;
55
- const timestamp = now();
56
- const result = await db.update(navItems).set({
57
- ...data.type !== undefined && {
58
- type: data.type
59
- },
60
- ...data.label !== undefined && {
61
- label: data.label
62
- },
63
- ...data.url !== undefined && {
64
- url: data.url
65
- },
66
- ...data.pageId !== undefined && {
67
- pageId: data.pageId
68
- },
69
- ...data.position !== undefined && {
70
- position: data.position
71
- },
72
- updatedAt: timestamp
73
- }).where(eq(navItems.id, id)).returning();
74
- return result[0] ? toNavItem(result[0]) : null;
75
- },
76
- async delete (id) {
77
- const result = await db.delete(navItems).where(eq(navItems.id, id)).returning();
78
- return result.length > 0;
79
- },
80
- async reorder (ids) {
81
- const timestamp = now();
82
- for(let i = 0; i < ids.length; i++){
83
- await db.update(navItems).set({
84
- position: i,
85
- updatedAt: timestamp
86
- })// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- loop index guarantees element exists
87
- .where(eq(navItems.id, ids[i]));
88
- }
89
- }
90
- };
91
- }