@jant/core 0.3.6 → 0.3.8

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 (264) hide show
  1. package/dist/app.js +11 -21
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +15 -0
  4. package/dist/i18n/locales/en.js +1 -1
  5. package/dist/i18n/locales/zh-Hans.js +1 -1
  6. package/dist/i18n/locales/zh-Hant.js +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/lib/image.js +3 -3
  9. package/dist/lib/media-helpers.js +43 -0
  10. package/dist/lib/nav-reorder.js +27 -0
  11. package/dist/lib/navigation.js +35 -0
  12. package/dist/lib/schemas.js +32 -2
  13. package/dist/lib/sse.js +7 -8
  14. package/dist/lib/theme-components.js +49 -0
  15. package/dist/routes/api/posts.js +101 -5
  16. package/dist/routes/api/timeline.js +115 -0
  17. package/dist/routes/api/upload.js +9 -5
  18. package/dist/routes/dash/media.js +38 -0
  19. package/dist/routes/dash/navigation.js +274 -0
  20. package/dist/routes/dash/posts.js +45 -6
  21. package/dist/routes/feed/rss.js +10 -1
  22. package/dist/routes/pages/archive.js +14 -27
  23. package/dist/routes/pages/collection.js +10 -19
  24. package/dist/routes/pages/home.js +88 -98
  25. package/dist/routes/pages/page.js +19 -38
  26. package/dist/routes/pages/post.js +61 -48
  27. package/dist/routes/pages/search.js +13 -26
  28. package/dist/services/collection.js +13 -0
  29. package/dist/services/index.js +3 -1
  30. package/dist/services/media.js +55 -2
  31. package/dist/services/navigation.js +115 -0
  32. package/dist/services/post.js +26 -1
  33. package/dist/theme/components/MediaGallery.js +107 -0
  34. package/dist/theme/components/PostForm.js +158 -2
  35. package/dist/theme/components/PostList.js +5 -0
  36. package/dist/theme/components/index.js +3 -0
  37. package/dist/theme/components/timeline/ArticleCard.js +50 -0
  38. package/dist/theme/components/timeline/ImageCard.js +86 -0
  39. package/dist/theme/components/timeline/LinkCard.js +62 -0
  40. package/dist/theme/components/timeline/NoteCard.js +37 -0
  41. package/dist/theme/components/timeline/QuoteCard.js +51 -0
  42. package/dist/theme/components/timeline/ThreadPreview.js +52 -0
  43. package/dist/theme/components/timeline/TimelineFeed.js +43 -0
  44. package/dist/theme/components/timeline/TimelineItem.js +25 -0
  45. package/dist/theme/components/timeline/index.js +8 -0
  46. package/dist/theme/layouts/DashLayout.js +8 -0
  47. package/dist/theme/layouts/SiteLayout.js +160 -0
  48. package/dist/theme/layouts/index.js +1 -0
  49. package/dist/types/sortablejs.d.js +5 -0
  50. package/dist/types.js +27 -0
  51. package/package.json +3 -2
  52. package/src/__tests__/helpers/app.ts +6 -1
  53. package/src/__tests__/helpers/db.ts +20 -0
  54. package/src/app.tsx +11 -25
  55. package/src/client.ts +1 -0
  56. package/src/db/migrations/0002_add_media_attachments.sql +3 -0
  57. package/src/db/migrations/0003_add_navigation_links.sql +8 -0
  58. package/src/db/migrations/meta/0003_snapshot.json +821 -0
  59. package/src/db/migrations/meta/_journal.json +14 -0
  60. package/src/db/schema.ts +15 -0
  61. package/src/i18n/locales/en.po +170 -58
  62. package/src/i18n/locales/en.ts +1 -1
  63. package/src/i18n/locales/zh-Hans.po +162 -71
  64. package/src/i18n/locales/zh-Hans.ts +1 -1
  65. package/src/i18n/locales/zh-Hant.po +162 -71
  66. package/src/i18n/locales/zh-Hant.ts +1 -1
  67. package/src/index.ts +13 -1
  68. package/src/lib/__tests__/schemas.test.ts +89 -1
  69. package/src/lib/__tests__/sse.test.ts +13 -1
  70. package/src/lib/__tests__/theme-components.test.ts +107 -0
  71. package/src/lib/image.ts +3 -3
  72. package/src/lib/media-helpers.ts +54 -0
  73. package/src/lib/nav-reorder.ts +26 -0
  74. package/src/lib/navigation.ts +46 -0
  75. package/src/lib/schemas.ts +47 -1
  76. package/src/lib/sse.ts +10 -11
  77. package/src/lib/theme-components.ts +76 -0
  78. package/src/routes/api/__tests__/posts.test.ts +239 -0
  79. package/src/routes/api/__tests__/timeline.test.ts +242 -0
  80. package/src/routes/api/posts.ts +134 -5
  81. package/src/routes/api/timeline.tsx +145 -0
  82. package/src/routes/api/upload.ts +9 -5
  83. package/src/routes/dash/media.tsx +50 -0
  84. package/src/routes/dash/navigation.tsx +306 -0
  85. package/src/routes/dash/posts.tsx +79 -7
  86. package/src/routes/feed/rss.ts +14 -1
  87. package/src/routes/pages/archive.tsx +15 -23
  88. package/src/routes/pages/collection.tsx +8 -15
  89. package/src/routes/pages/home.tsx +121 -88
  90. package/src/routes/pages/page.tsx +17 -30
  91. package/src/routes/pages/post.tsx +64 -40
  92. package/src/routes/pages/search.tsx +18 -22
  93. package/src/services/__tests__/collection.test.ts +102 -0
  94. package/src/services/__tests__/media.test.ts +282 -7
  95. package/src/services/__tests__/navigation.test.ts +213 -0
  96. package/src/services/__tests__/post-timeline.test.ts +220 -0
  97. package/src/services/collection.ts +19 -0
  98. package/src/services/index.ts +7 -0
  99. package/src/services/media.ts +78 -2
  100. package/src/services/navigation.ts +165 -0
  101. package/src/services/post.ts +48 -1
  102. package/src/styles/components.css +59 -0
  103. package/src/theme/components/MediaGallery.tsx +128 -0
  104. package/src/theme/components/PostForm.tsx +170 -2
  105. package/src/theme/components/PostList.tsx +7 -0
  106. package/src/theme/components/index.ts +13 -0
  107. package/src/theme/components/timeline/ArticleCard.tsx +57 -0
  108. package/src/theme/components/timeline/ImageCard.tsx +80 -0
  109. package/src/theme/components/timeline/LinkCard.tsx +66 -0
  110. package/src/theme/components/timeline/NoteCard.tsx +41 -0
  111. package/src/theme/components/timeline/QuoteCard.tsx +55 -0
  112. package/src/theme/components/timeline/ThreadPreview.tsx +49 -0
  113. package/src/theme/components/timeline/TimelineFeed.tsx +52 -0
  114. package/src/theme/components/timeline/TimelineItem.tsx +39 -0
  115. package/src/theme/components/timeline/index.ts +8 -0
  116. package/src/theme/layouts/DashLayout.tsx +10 -0
  117. package/src/theme/layouts/SiteLayout.tsx +184 -0
  118. package/src/theme/layouts/index.ts +1 -0
  119. package/src/types/sortablejs.d.ts +23 -0
  120. package/src/types.ts +97 -0
  121. package/dist/app.d.ts +0 -38
  122. package/dist/app.d.ts.map +0 -1
  123. package/dist/auth.d.ts +0 -25
  124. package/dist/auth.d.ts.map +0 -1
  125. package/dist/db/index.d.ts +0 -10
  126. package/dist/db/index.d.ts.map +0 -1
  127. package/dist/db/schema.d.ts +0 -1507
  128. package/dist/db/schema.d.ts.map +0 -1
  129. package/dist/i18n/Trans.d.ts +0 -25
  130. package/dist/i18n/Trans.d.ts.map +0 -1
  131. package/dist/i18n/context.d.ts +0 -69
  132. package/dist/i18n/context.d.ts.map +0 -1
  133. package/dist/i18n/detect.d.ts +0 -20
  134. package/dist/i18n/detect.d.ts.map +0 -1
  135. package/dist/i18n/i18n.d.ts +0 -32
  136. package/dist/i18n/i18n.d.ts.map +0 -1
  137. package/dist/i18n/index.d.ts +0 -41
  138. package/dist/i18n/index.d.ts.map +0 -1
  139. package/dist/i18n/locales/en.d.ts +0 -3
  140. package/dist/i18n/locales/en.d.ts.map +0 -1
  141. package/dist/i18n/locales/zh-Hans.d.ts +0 -3
  142. package/dist/i18n/locales/zh-Hans.d.ts.map +0 -1
  143. package/dist/i18n/locales/zh-Hant.d.ts +0 -3
  144. package/dist/i18n/locales/zh-Hant.d.ts.map +0 -1
  145. package/dist/i18n/locales.d.ts +0 -11
  146. package/dist/i18n/locales.d.ts.map +0 -1
  147. package/dist/i18n/middleware.d.ts +0 -21
  148. package/dist/i18n/middleware.d.ts.map +0 -1
  149. package/dist/index.d.ts +0 -16
  150. package/dist/index.d.ts.map +0 -1
  151. package/dist/lib/config.d.ts +0 -83
  152. package/dist/lib/config.d.ts.map +0 -1
  153. package/dist/lib/constants.d.ts +0 -37
  154. package/dist/lib/constants.d.ts.map +0 -1
  155. package/dist/lib/image.d.ts +0 -73
  156. package/dist/lib/image.d.ts.map +0 -1
  157. package/dist/lib/index.d.ts +0 -9
  158. package/dist/lib/index.d.ts.map +0 -1
  159. package/dist/lib/markdown.d.ts +0 -60
  160. package/dist/lib/markdown.d.ts.map +0 -1
  161. package/dist/lib/schemas.d.ts +0 -113
  162. package/dist/lib/schemas.d.ts.map +0 -1
  163. package/dist/lib/sqid.d.ts +0 -60
  164. package/dist/lib/sqid.d.ts.map +0 -1
  165. package/dist/lib/sse.d.ts +0 -192
  166. package/dist/lib/sse.d.ts.map +0 -1
  167. package/dist/lib/theme.d.ts +0 -44
  168. package/dist/lib/theme.d.ts.map +0 -1
  169. package/dist/lib/time.d.ts +0 -90
  170. package/dist/lib/time.d.ts.map +0 -1
  171. package/dist/lib/url.d.ts +0 -82
  172. package/dist/lib/url.d.ts.map +0 -1
  173. package/dist/middleware/auth.d.ts +0 -24
  174. package/dist/middleware/auth.d.ts.map +0 -1
  175. package/dist/middleware/onboarding.d.ts +0 -26
  176. package/dist/middleware/onboarding.d.ts.map +0 -1
  177. package/dist/routes/api/posts.d.ts +0 -13
  178. package/dist/routes/api/posts.d.ts.map +0 -1
  179. package/dist/routes/api/search.d.ts +0 -13
  180. package/dist/routes/api/search.d.ts.map +0 -1
  181. package/dist/routes/api/upload.d.ts +0 -16
  182. package/dist/routes/api/upload.d.ts.map +0 -1
  183. package/dist/routes/dash/collections.d.ts +0 -13
  184. package/dist/routes/dash/collections.d.ts.map +0 -1
  185. package/dist/routes/dash/index.d.ts +0 -15
  186. package/dist/routes/dash/index.d.ts.map +0 -1
  187. package/dist/routes/dash/media.d.ts +0 -16
  188. package/dist/routes/dash/media.d.ts.map +0 -1
  189. package/dist/routes/dash/pages.d.ts +0 -15
  190. package/dist/routes/dash/pages.d.ts.map +0 -1
  191. package/dist/routes/dash/posts.d.ts +0 -13
  192. package/dist/routes/dash/posts.d.ts.map +0 -1
  193. package/dist/routes/dash/redirects.d.ts +0 -13
  194. package/dist/routes/dash/redirects.d.ts.map +0 -1
  195. package/dist/routes/dash/settings.d.ts +0 -15
  196. package/dist/routes/dash/settings.d.ts.map +0 -1
  197. package/dist/routes/feed/rss.d.ts +0 -13
  198. package/dist/routes/feed/rss.d.ts.map +0 -1
  199. package/dist/routes/feed/sitemap.d.ts +0 -13
  200. package/dist/routes/feed/sitemap.d.ts.map +0 -1
  201. package/dist/routes/pages/archive.d.ts +0 -15
  202. package/dist/routes/pages/archive.d.ts.map +0 -1
  203. package/dist/routes/pages/collection.d.ts +0 -13
  204. package/dist/routes/pages/collection.d.ts.map +0 -1
  205. package/dist/routes/pages/home.d.ts +0 -13
  206. package/dist/routes/pages/home.d.ts.map +0 -1
  207. package/dist/routes/pages/page.d.ts +0 -15
  208. package/dist/routes/pages/page.d.ts.map +0 -1
  209. package/dist/routes/pages/post.d.ts +0 -13
  210. package/dist/routes/pages/post.d.ts.map +0 -1
  211. package/dist/routes/pages/search.d.ts +0 -13
  212. package/dist/routes/pages/search.d.ts.map +0 -1
  213. package/dist/services/collection.d.ts +0 -31
  214. package/dist/services/collection.d.ts.map +0 -1
  215. package/dist/services/index.d.ts +0 -28
  216. package/dist/services/index.d.ts.map +0 -1
  217. package/dist/services/media.d.ts +0 -27
  218. package/dist/services/media.d.ts.map +0 -1
  219. package/dist/services/post.d.ts +0 -31
  220. package/dist/services/post.d.ts.map +0 -1
  221. package/dist/services/redirect.d.ts +0 -15
  222. package/dist/services/redirect.d.ts.map +0 -1
  223. package/dist/services/search.d.ts +0 -26
  224. package/dist/services/search.d.ts.map +0 -1
  225. package/dist/services/settings.d.ts +0 -18
  226. package/dist/services/settings.d.ts.map +0 -1
  227. package/dist/theme/color-themes.d.ts +0 -30
  228. package/dist/theme/color-themes.d.ts.map +0 -1
  229. package/dist/theme/components/ActionButtons.d.ts +0 -43
  230. package/dist/theme/components/ActionButtons.d.ts.map +0 -1
  231. package/dist/theme/components/CrudPageHeader.d.ts +0 -23
  232. package/dist/theme/components/CrudPageHeader.d.ts.map +0 -1
  233. package/dist/theme/components/DangerZone.d.ts +0 -36
  234. package/dist/theme/components/DangerZone.d.ts.map +0 -1
  235. package/dist/theme/components/EmptyState.d.ts +0 -27
  236. package/dist/theme/components/EmptyState.d.ts.map +0 -1
  237. package/dist/theme/components/ListItemRow.d.ts +0 -15
  238. package/dist/theme/components/ListItemRow.d.ts.map +0 -1
  239. package/dist/theme/components/PageForm.d.ts +0 -14
  240. package/dist/theme/components/PageForm.d.ts.map +0 -1
  241. package/dist/theme/components/Pagination.d.ts +0 -46
  242. package/dist/theme/components/Pagination.d.ts.map +0 -1
  243. package/dist/theme/components/PostForm.d.ts +0 -11
  244. package/dist/theme/components/PostForm.d.ts.map +0 -1
  245. package/dist/theme/components/PostList.d.ts +0 -10
  246. package/dist/theme/components/PostList.d.ts.map +0 -1
  247. package/dist/theme/components/ThreadView.d.ts +0 -15
  248. package/dist/theme/components/ThreadView.d.ts.map +0 -1
  249. package/dist/theme/components/TypeBadge.d.ts +0 -12
  250. package/dist/theme/components/TypeBadge.d.ts.map +0 -1
  251. package/dist/theme/components/VisibilityBadge.d.ts +0 -12
  252. package/dist/theme/components/VisibilityBadge.d.ts.map +0 -1
  253. package/dist/theme/components/index.d.ts +0 -13
  254. package/dist/theme/components/index.d.ts.map +0 -1
  255. package/dist/theme/index.d.ts +0 -21
  256. package/dist/theme/index.d.ts.map +0 -1
  257. package/dist/theme/layouts/BaseLayout.d.ts +0 -23
  258. package/dist/theme/layouts/BaseLayout.d.ts.map +0 -1
  259. package/dist/theme/layouts/DashLayout.d.ts +0 -17
  260. package/dist/theme/layouts/DashLayout.d.ts.map +0 -1
  261. package/dist/theme/layouts/index.d.ts +0 -3
  262. package/dist/theme/layouts/index.d.ts.map +0 -1
  263. package/dist/types.d.ts +0 -213
  264. package/dist/types.d.ts.map +0 -1
@@ -1,61 +1,50 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
- import { getSiteName } from "../../lib/config.js";
3
2
  /**
4
3
  * Single Post Page Route
5
4
  */ import { Hono } from "hono";
6
5
  import { useLingui as $_useLingui } from "@jant/core/i18n";
7
- import { BaseLayout } from "../../theme/layouts/index.js";
6
+ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
7
+ import { MediaGallery } from "../../theme/components/index.js";
8
8
  import * as sqid from "../../lib/sqid.js";
9
9
  import * as time from "../../lib/time.js";
10
+ import { getMediaUrl, getImageUrl } from "../../lib/image.js";
11
+ import { getNavigationData } from "../../lib/navigation.js";
10
12
  export const postRoutes = new Hono();
11
- function PostContent({ post }) {
13
+ function PostContent({ post, mediaAttachments }) {
12
14
  const { i18n: $__i18n, _: $__ } = $_useLingui();
13
- return /*#__PURE__*/ _jsxs("div", {
14
- class: "container py-8",
15
+ return /*#__PURE__*/ _jsxs("article", {
16
+ class: "h-entry",
15
17
  children: [
16
- /*#__PURE__*/ _jsxs("article", {
17
- class: "h-entry",
18
+ post.title && /*#__PURE__*/ _jsx("h1", {
19
+ class: "p-name text-2xl font-semibold mb-4",
20
+ children: post.title
21
+ }),
22
+ /*#__PURE__*/ _jsx("div", {
23
+ class: "e-content prose",
24
+ dangerouslySetInnerHTML: {
25
+ __html: post.contentHtml || ""
26
+ }
27
+ }),
28
+ mediaAttachments.length > 0 && /*#__PURE__*/ _jsx(MediaGallery, {
29
+ attachments: mediaAttachments
30
+ }),
31
+ /*#__PURE__*/ _jsxs("footer", {
32
+ class: "mt-6 pt-4 border-t text-sm text-muted-foreground",
18
33
  children: [
19
- post.title && /*#__PURE__*/ _jsx("h1", {
20
- class: "p-name text-2xl font-semibold mb-4",
21
- children: post.title
22
- }),
23
- /*#__PURE__*/ _jsx("div", {
24
- class: "e-content prose",
25
- dangerouslySetInnerHTML: {
26
- __html: post.contentHtml || ""
27
- }
34
+ /*#__PURE__*/ _jsx("time", {
35
+ class: "dt-published",
36
+ datetime: time.toISOString(post.publishedAt),
37
+ children: time.formatDate(post.publishedAt)
28
38
  }),
29
- /*#__PURE__*/ _jsxs("footer", {
30
- class: "mt-6 pt-4 border-t text-sm text-muted-foreground",
31
- children: [
32
- /*#__PURE__*/ _jsx("time", {
33
- class: "dt-published",
34
- datetime: time.toISOString(post.publishedAt),
35
- children: time.formatDate(post.publishedAt)
36
- }),
37
- /*#__PURE__*/ _jsx("a", {
38
- href: `/p/${sqid.encode(post.id)}`,
39
- class: "u-url ml-4",
40
- children: $__i18n._({
41
- id: "D9Oea+",
42
- message: "Permalink"
43
- })
44
- })
45
- ]
39
+ /*#__PURE__*/ _jsx("a", {
40
+ href: `/p/${sqid.encode(post.id)}`,
41
+ class: "u-url ml-4",
42
+ children: $__i18n._({
43
+ id: "D9Oea+",
44
+ message: "Permalink"
45
+ })
46
46
  })
47
47
  ]
48
- }),
49
- /*#__PURE__*/ _jsx("nav", {
50
- class: "mt-8",
51
- children: /*#__PURE__*/ _jsx("a", {
52
- href: "/",
53
- class: "text-sm hover:underline",
54
- children: $__i18n._({
55
- id: "biOepV",
56
- message: "← Back to home"
57
- })
58
- })
59
48
  })
60
49
  ]
61
50
  });
@@ -78,14 +67,38 @@ postRoutes.get("/:id", async (c)=>{
78
67
  if (post.visibility === "draft") {
79
68
  return c.notFound();
80
69
  }
81
- const siteName = await getSiteName(c);
82
- const title = post.title || siteName;
70
+ // Load media attachments
71
+ const rawMedia = await c.var.services.media.getByPostId(post.id);
72
+ const r2PublicUrl = c.env.R2_PUBLIC_URL;
73
+ const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
74
+ const mediaAttachments = rawMedia.map((m)=>({
75
+ id: m.id,
76
+ url: getMediaUrl(m.id, m.r2Key, r2PublicUrl),
77
+ previewUrl: getImageUrl(getMediaUrl(m.id, m.r2Key, r2PublicUrl), imageTransformUrl, {
78
+ width: 400,
79
+ quality: 80,
80
+ format: "auto",
81
+ fit: "cover"
82
+ }),
83
+ alt: m.alt,
84
+ blurhash: m.blurhash,
85
+ width: m.width,
86
+ height: m.height,
87
+ position: m.position,
88
+ mimeType: m.mimeType
89
+ }));
90
+ const navData = await getNavigationData(c);
91
+ const title = post.title || navData.siteName;
83
92
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
84
93
  title: title,
85
94
  description: post.content?.slice(0, 160),
86
95
  c: c,
87
- children: /*#__PURE__*/ _jsx(PostContent, {
88
- post: post
96
+ children: /*#__PURE__*/ _jsx(SiteLayout, {
97
+ ...navData,
98
+ children: /*#__PURE__*/ _jsx(PostContent, {
99
+ post: post,
100
+ mediaAttachments: mediaAttachments
101
+ })
89
102
  })
90
103
  }));
91
104
  });
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
2
- import { getSiteName } from "../../lib/config.js";
3
2
  /**
4
3
  * Search Page Route
5
4
  */ import { Hono } from "hono";
6
5
  import { useLingui as $_useLingui } from "@jant/core/i18n";
7
- import { BaseLayout } from "../../theme/layouts/index.js";
6
+ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
8
7
  import { PagePagination } from "../../theme/components/index.js";
9
8
  import * as sqid from "../../lib/sqid.js";
10
9
  import * as time from "../../lib/time.js";
10
+ import { getNavigationData } from "../../lib/navigation.js";
11
11
  const PAGE_SIZE = 10;
12
12
  export const searchRoutes = new Hono();
13
13
  function SearchContent({ query, results, error, hasMore, page }) {
@@ -17,7 +17,6 @@ function SearchContent({ query, results, error, hasMore, page }) {
17
17
  message: "Search"
18
18
  });
19
19
  return /*#__PURE__*/ _jsxs("div", {
20
- class: "container py-8 max-w-2xl",
21
20
  children: [
22
21
  /*#__PURE__*/ _jsx("h1", {
23
22
  class: "text-2xl font-semibold mb-6",
@@ -118,21 +117,6 @@ function SearchContent({ query, results, error, hasMore, page }) {
118
117
  ]
119
118
  })
120
119
  ]
121
- }),
122
- /*#__PURE__*/ _jsx("nav", {
123
- class: "mt-8 pt-6 border-t",
124
- children: /*#__PURE__*/ _jsxs("a", {
125
- href: "/",
126
- class: "text-sm hover:underline",
127
- children: [
128
- "←",
129
- " ",
130
- $__i18n._({
131
- id: "x4RuFo",
132
- message: "Back to home"
133
- })
134
- ]
135
- })
136
120
  })
137
121
  ]
138
122
  });
@@ -141,7 +125,7 @@ searchRoutes.get("/", async (c)=>{
141
125
  const query = c.req.query("q") || "";
142
126
  const pageParam = c.req.query("page");
143
127
  const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
144
- const siteName = await getSiteName(c);
128
+ const navData = await getNavigationData(c);
145
129
  // Only search if there's a query
146
130
  let results = [];
147
131
  let error = null;
@@ -168,14 +152,17 @@ searchRoutes.get("/", async (c)=>{
168
152
  }
169
153
  }
170
154
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
171
- title: query ? `Search: ${query} - ${siteName}` : `Search - ${siteName}`,
155
+ title: query ? `Search: ${query} - ${navData.siteName}` : `Search - ${navData.siteName}`,
172
156
  c: c,
173
- children: /*#__PURE__*/ _jsx(SearchContent, {
174
- query: query,
175
- results: results,
176
- error: error,
177
- hasMore: hasMore,
178
- page: page
157
+ children: /*#__PURE__*/ _jsx(SiteLayout, {
158
+ ...navData,
159
+ children: /*#__PURE__*/ _jsx(SearchContent, {
160
+ query: query,
161
+ results: results,
162
+ error: error,
163
+ hasMore: hasMore,
164
+ page: page
165
+ })
179
166
  })
180
167
  }));
181
168
  });
@@ -103,6 +103,19 @@ export function createCollectionService(db) {
103
103
  collection: collections
104
104
  }).from(postCollections).innerJoin(collections, eq(postCollections.collectionId, collections.id)).where(eq(postCollections.postId, postId));
105
105
  return rows.map((r)=>toCollection(r.collection));
106
+ },
107
+ async syncPostCollections (postId, collectionIds) {
108
+ const current = await this.getCollectionsForPost(postId);
109
+ const currentIds = new Set(current.map((c)=>c.id));
110
+ const desiredIds = new Set(collectionIds);
111
+ const toAdd = collectionIds.filter((id)=>!currentIds.has(id));
112
+ const toRemove = current.map((c)=>c.id).filter((id)=>!desiredIds.has(id));
113
+ for (const collectionId of toAdd){
114
+ await this.addPost(collectionId, postId);
115
+ }
116
+ for (const collectionId of toRemove){
117
+ await this.removePost(collectionId, postId);
118
+ }
106
119
  }
107
120
  };
108
121
  }
@@ -8,6 +8,7 @@ import { createRedirectService } from "./redirect.js";
8
8
  import { createMediaService } from "./media.js";
9
9
  import { createCollectionService } from "./collection.js";
10
10
  import { createSearchService } from "./search.js";
11
+ import { createNavigationLinkService } from "./navigation.js";
11
12
  export function createServices(db, d1) {
12
13
  return {
13
14
  settings: createSettingsService(db),
@@ -15,6 +16,7 @@ export function createServices(db, d1) {
15
16
  redirects: createRedirectService(db),
16
17
  media: createMediaService(db),
17
18
  collections: createCollectionService(db),
18
- search: createSearchService(d1)
19
+ search: createSearchService(d1),
20
+ navigationLinks: createNavigationLinkService(db)
19
21
  };
20
22
  }
@@ -2,7 +2,7 @@
2
2
  * Media Service
3
3
  *
4
4
  * Handles media upload and management with R2 storage
5
- */ import { eq, desc } from "drizzle-orm";
5
+ */ import { eq, desc, inArray, asc } from "drizzle-orm";
6
6
  import { uuidv7 } from "uuidv7";
7
7
  import { media } from "../db/schema.js";
8
8
  import { now } from "../lib/time.js";
@@ -19,6 +19,8 @@ export function createMediaService(db) {
19
19
  width: row.width,
20
20
  height: row.height,
21
21
  alt: row.alt,
22
+ position: row.position,
23
+ blurhash: row.blurhash,
22
24
  createdAt: row.createdAt
23
25
  };
24
26
  }
@@ -27,6 +29,33 @@ export function createMediaService(db) {
27
29
  const result = await db.select().from(media).where(eq(media.id, id)).limit(1);
28
30
  return result[0] ? toMedia(result[0]) : null;
29
31
  },
32
+ async getByIds (ids) {
33
+ if (ids.length === 0) return [];
34
+ const rows = await db.select().from(media).where(inArray(media.id, ids));
35
+ return rows.map(toMedia);
36
+ },
37
+ async getByPostId (postId) {
38
+ const rows = await db.select().from(media).where(eq(media.postId, postId)).orderBy(asc(media.position));
39
+ return rows.map(toMedia);
40
+ },
41
+ async getByPostIds (postIds) {
42
+ const result = new Map();
43
+ if (postIds.length === 0) return result;
44
+ const rows = await db.select().from(media).where(inArray(media.postId, postIds)).orderBy(asc(media.position));
45
+ for (const row of rows){
46
+ const m = toMedia(row);
47
+ if (m.postId === null) continue;
48
+ const list = result.get(m.postId);
49
+ if (list) {
50
+ list.push(m);
51
+ } else {
52
+ result.set(m.postId, [
53
+ m
54
+ ]);
55
+ }
56
+ }
57
+ return result;
58
+ },
30
59
  async getByR2Key (r2Key) {
31
60
  const result = await db.select().from(media).where(eq(media.r2Key, r2Key)).limit(1);
32
61
  return result[0] ? toMedia(result[0]) : null;
@@ -36,7 +65,7 @@ export function createMediaService(db) {
36
65
  return rows.map(toMedia);
37
66
  },
38
67
  async create (data) {
39
- const id = uuidv7();
68
+ const id = data.id ?? uuidv7();
40
69
  const timestamp = now();
41
70
  const result = await db.insert(media).values({
42
71
  id,
@@ -49,11 +78,35 @@ export function createMediaService(db) {
49
78
  width: data.width ?? null,
50
79
  height: data.height ?? null,
51
80
  alt: data.alt ?? null,
81
+ position: data.position ?? 0,
82
+ blurhash: data.blurhash ?? null,
52
83
  createdAt: timestamp
53
84
  }).returning();
54
85
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
55
86
  return toMedia(result[0]);
56
87
  },
88
+ async attachToPost (postId, mediaIds) {
89
+ // Clear existing attachments
90
+ await db.update(media).set({
91
+ postId: null,
92
+ position: 0
93
+ }).where(eq(media.postId, postId));
94
+ // Set new attachments with position = array index
95
+ for(let i = 0; i < mediaIds.length; i++){
96
+ const mediaId = mediaIds[i];
97
+ if (!mediaId) continue;
98
+ await db.update(media).set({
99
+ postId,
100
+ position: i
101
+ }).where(eq(media.id, mediaId));
102
+ }
103
+ },
104
+ async detachFromPost (postId) {
105
+ await db.update(media).set({
106
+ postId: null,
107
+ position: 0
108
+ }).where(eq(media.postId, postId));
109
+ },
57
110
  async delete (id) {
58
111
  const result = await db.delete(media).where(eq(media.id, id)).returning();
59
112
  return result.length > 0;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Navigation Link Service
3
+ *
4
+ * Manages navigation links displayed on public pages
5
+ */ import { eq, asc, sql } from "drizzle-orm";
6
+ import { navigationLinks } from "../db/schema.js";
7
+ import { now } from "../lib/time.js";
8
+ export function createNavigationLinkService(db) {
9
+ function toNavigationLink(row) {
10
+ return {
11
+ id: row.id,
12
+ label: row.label,
13
+ url: row.url,
14
+ position: row.position,
15
+ createdAt: row.createdAt,
16
+ updatedAt: row.updatedAt
17
+ };
18
+ }
19
+ return {
20
+ async list () {
21
+ const rows = await db.select().from(navigationLinks).orderBy(asc(navigationLinks.position));
22
+ return rows.map(toNavigationLink);
23
+ },
24
+ async getById (id) {
25
+ const result = await db.select().from(navigationLinks).where(eq(navigationLinks.id, id)).limit(1);
26
+ return result[0] ? toNavigationLink(result[0]) : null;
27
+ },
28
+ async create (data) {
29
+ const timestamp = now();
30
+ let position = data.position;
31
+ if (position === undefined) {
32
+ const maxResult = await db.select({
33
+ maxPos: sql`COALESCE(MAX(position), -1)`
34
+ }).from(navigationLinks);
35
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- aggregate always returns one row
36
+ position = maxResult[0].maxPos + 1;
37
+ }
38
+ const result = await db.insert(navigationLinks).values({
39
+ label: data.label,
40
+ url: data.url,
41
+ position,
42
+ createdAt: timestamp,
43
+ updatedAt: timestamp
44
+ }).returning();
45
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
46
+ return toNavigationLink(result[0]);
47
+ },
48
+ async update (id, data) {
49
+ const existing = await db.select().from(navigationLinks).where(eq(navigationLinks.id, id)).limit(1);
50
+ if (!existing[0]) return null;
51
+ const timestamp = now();
52
+ const result = await db.update(navigationLinks).set({
53
+ ...data.label !== undefined && {
54
+ label: data.label
55
+ },
56
+ ...data.url !== undefined && {
57
+ url: data.url
58
+ },
59
+ ...data.position !== undefined && {
60
+ position: data.position
61
+ },
62
+ updatedAt: timestamp
63
+ }).where(eq(navigationLinks.id, id)).returning();
64
+ return result[0] ? toNavigationLink(result[0]) : null;
65
+ },
66
+ async delete (id) {
67
+ const result = await db.delete(navigationLinks).where(eq(navigationLinks.id, id)).returning();
68
+ return result.length > 0;
69
+ },
70
+ async reorder (ids) {
71
+ const timestamp = now();
72
+ for(let i = 0; i < ids.length; i++){
73
+ await db.update(navigationLinks).set({
74
+ position: i,
75
+ updatedAt: timestamp
76
+ })// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- loop index guarantees element exists
77
+ .where(eq(navigationLinks.id, ids[i]));
78
+ }
79
+ },
80
+ async ensureDefaults () {
81
+ const existing = await db.select().from(navigationLinks).limit(1);
82
+ if (existing.length > 0) {
83
+ const rows = await db.select().from(navigationLinks).orderBy(asc(navigationLinks.position));
84
+ return rows.map(toNavigationLink);
85
+ }
86
+ const timestamp = now();
87
+ const defaults = [
88
+ {
89
+ label: "Home",
90
+ url: "/",
91
+ position: 0
92
+ },
93
+ {
94
+ label: "Archive",
95
+ url: "/archive",
96
+ position: 1
97
+ },
98
+ {
99
+ label: "RSS",
100
+ url: "/feed",
101
+ position: 2
102
+ }
103
+ ];
104
+ for (const link of defaults){
105
+ await db.insert(navigationLinks).values({
106
+ ...link,
107
+ createdAt: timestamp,
108
+ updatedAt: timestamp
109
+ });
110
+ }
111
+ const rows = await db.select().from(navigationLinks).orderBy(asc(navigationLinks.position));
112
+ return rows.map(toNavigationLink);
113
+ }
114
+ };
115
+ }
@@ -2,7 +2,7 @@
2
2
  * Post Service
3
3
  *
4
4
  * CRUD operations for posts with Thread support
5
- */ import { eq, and, isNull, desc, or, inArray, sql } from "drizzle-orm";
5
+ */ import { eq, and, isNull, desc, or, inArray, notInArray, sql } from "drizzle-orm";
6
6
  import { posts } from "../db/schema.js";
7
7
  import { now } from "../lib/time.js";
8
8
  import { extractDomain } from "../lib/url.js";
@@ -52,6 +52,10 @@ export function createPostService(db) {
52
52
  if (filters.type) {
53
53
  conditions.push(eq(posts.type, filters.type));
54
54
  }
55
+ // Exclude types filter
56
+ if (filters.excludeTypes && filters.excludeTypes.length > 0) {
57
+ conditions.push(notInArray(posts.type, filters.excludeTypes));
58
+ }
55
59
  // Thread filter
56
60
  if (filters.threadId) {
57
61
  conditions.push(eq(posts.threadId, filters.threadId));
@@ -186,6 +190,27 @@ export function createPostService(db) {
186
190
  }
187
191
  }
188
192
  return counts;
193
+ },
194
+ async getThreadPreviews (rootIds, previewCount = 3) {
195
+ if (rootIds.length === 0) return new Map();
196
+ const rows = await db.select().from(posts).where(and(inArray(posts.threadId, rootIds), isNull(posts.deletedAt))).orderBy(posts.threadId, posts.createdAt);
197
+ // Partition by threadId, take first previewCount per thread
198
+ const result = new Map();
199
+ for (const row of rows){
200
+ const post = toPost(row);
201
+ if (post.threadId === null) continue;
202
+ const list = result.get(post.threadId);
203
+ if (list) {
204
+ if (list.length < previewCount) {
205
+ list.push(post);
206
+ }
207
+ } else {
208
+ result.set(post.threadId, [
209
+ post
210
+ ]);
211
+ }
212
+ }
213
+ return result;
189
214
  }
190
215
  };
191
216
  }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Media Gallery Component
3
+ *
4
+ * Renders media attachments on public post pages.
5
+ * Layout adapts based on the number of images.
6
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
7
+ export const MediaGallery = ({ attachments })=>{
8
+ const images = attachments.filter((a)=>a.mimeType.startsWith("image/"));
9
+ if (images.length === 0) return null;
10
+ if (images.length === 1) {
11
+ const [img] = images;
12
+ if (!img) return null;
13
+ return /*#__PURE__*/ _jsx("div", {
14
+ class: "mt-3",
15
+ children: /*#__PURE__*/ _jsx("a", {
16
+ href: img.url,
17
+ target: "_blank",
18
+ rel: "noopener noreferrer",
19
+ children: /*#__PURE__*/ _jsx("img", {
20
+ src: img.previewUrl,
21
+ alt: img.alt || "",
22
+ width: img.width ?? undefined,
23
+ height: img.height ?? undefined,
24
+ class: "rounded-lg max-w-full h-auto",
25
+ loading: "lazy"
26
+ })
27
+ })
28
+ });
29
+ }
30
+ if (images.length === 2) {
31
+ return /*#__PURE__*/ _jsx("div", {
32
+ class: "mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden",
33
+ children: images.map((img)=>/*#__PURE__*/ _jsx("a", {
34
+ href: img.url,
35
+ target: "_blank",
36
+ rel: "noopener noreferrer",
37
+ class: "aspect-square",
38
+ children: /*#__PURE__*/ _jsx("img", {
39
+ src: img.previewUrl,
40
+ alt: img.alt || "",
41
+ class: "w-full h-full object-cover",
42
+ loading: "lazy"
43
+ })
44
+ }, img.id))
45
+ });
46
+ }
47
+ if (images.length === 3) {
48
+ const [first, ...rest] = images;
49
+ if (!first) return null;
50
+ return /*#__PURE__*/ _jsxs("div", {
51
+ class: "mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden",
52
+ children: [
53
+ /*#__PURE__*/ _jsx("a", {
54
+ href: first.url,
55
+ target: "_blank",
56
+ rel: "noopener noreferrer",
57
+ class: "row-span-2",
58
+ children: /*#__PURE__*/ _jsx("img", {
59
+ src: first.previewUrl,
60
+ alt: first.alt || "",
61
+ class: "w-full h-full object-cover",
62
+ loading: "lazy"
63
+ })
64
+ }),
65
+ rest.map((img)=>/*#__PURE__*/ _jsx("a", {
66
+ href: img.url,
67
+ target: "_blank",
68
+ rel: "noopener noreferrer",
69
+ class: "aspect-square",
70
+ children: /*#__PURE__*/ _jsx("img", {
71
+ src: img.previewUrl,
72
+ alt: img.alt || "",
73
+ class: "w-full h-full object-cover",
74
+ loading: "lazy"
75
+ })
76
+ }, img.id))
77
+ ]
78
+ });
79
+ }
80
+ // 4+ images: 2-column grid, show first 4 with remaining count
81
+ const shown = images.slice(0, 4);
82
+ const remaining = images.length - 4;
83
+ return /*#__PURE__*/ _jsx("div", {
84
+ class: "mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden",
85
+ children: shown.map((img, i)=>/*#__PURE__*/ _jsxs("a", {
86
+ href: img.url,
87
+ target: "_blank",
88
+ rel: "noopener noreferrer",
89
+ class: "relative aspect-square",
90
+ children: [
91
+ /*#__PURE__*/ _jsx("img", {
92
+ src: img.previewUrl,
93
+ alt: img.alt || "",
94
+ class: "w-full h-full object-cover",
95
+ loading: "lazy"
96
+ }),
97
+ i === 3 && remaining > 0 && /*#__PURE__*/ _jsxs("div", {
98
+ class: "absolute inset-0 bg-black/50 flex items-center justify-center text-white text-xl font-semibold",
99
+ children: [
100
+ "+",
101
+ remaining
102
+ ]
103
+ })
104
+ ]
105
+ }, img.id))
106
+ });
107
+ };