@jant/core 0.3.7 → 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 (241) hide show
  1. package/dist/app.js +4 -0
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +13 -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/lib/image.js +3 -3
  8. package/dist/lib/media-helpers.js +43 -0
  9. package/dist/lib/nav-reorder.js +27 -0
  10. package/dist/lib/navigation.js +35 -0
  11. package/dist/lib/theme-components.js +49 -0
  12. package/dist/routes/api/timeline.js +115 -0
  13. package/dist/routes/api/upload.js +9 -5
  14. package/dist/routes/dash/navigation.js +274 -0
  15. package/dist/routes/pages/archive.js +14 -27
  16. package/dist/routes/pages/collection.js +10 -19
  17. package/dist/routes/pages/home.js +83 -126
  18. package/dist/routes/pages/page.js +19 -38
  19. package/dist/routes/pages/post.js +38 -51
  20. package/dist/routes/pages/search.js +13 -26
  21. package/dist/services/index.js +3 -1
  22. package/dist/services/media.js +1 -1
  23. package/dist/services/navigation.js +115 -0
  24. package/dist/services/post.js +26 -1
  25. package/dist/theme/components/PostList.js +5 -0
  26. package/dist/theme/components/index.js +2 -0
  27. package/dist/theme/components/timeline/ArticleCard.js +50 -0
  28. package/dist/theme/components/timeline/ImageCard.js +86 -0
  29. package/dist/theme/components/timeline/LinkCard.js +62 -0
  30. package/dist/theme/components/timeline/NoteCard.js +37 -0
  31. package/dist/theme/components/timeline/QuoteCard.js +51 -0
  32. package/dist/theme/components/timeline/ThreadPreview.js +52 -0
  33. package/dist/theme/components/timeline/TimelineFeed.js +43 -0
  34. package/dist/theme/components/timeline/TimelineItem.js +25 -0
  35. package/dist/theme/components/timeline/index.js +8 -0
  36. package/dist/theme/layouts/DashLayout.js +8 -0
  37. package/dist/theme/layouts/SiteLayout.js +160 -0
  38. package/dist/theme/layouts/index.js +1 -0
  39. package/dist/types/sortablejs.d.js +5 -0
  40. package/package.json +3 -2
  41. package/src/__tests__/helpers/db.ts +10 -0
  42. package/src/app.tsx +4 -0
  43. package/src/client.ts +1 -0
  44. package/src/db/migrations/0003_add_navigation_links.sql +8 -0
  45. package/src/db/migrations/meta/0003_snapshot.json +821 -0
  46. package/src/db/migrations/meta/_journal.json +14 -0
  47. package/src/db/schema.ts +13 -0
  48. package/src/i18n/locales/en.po +100 -32
  49. package/src/i18n/locales/en.ts +1 -1
  50. package/src/i18n/locales/zh-Hans.po +102 -55
  51. package/src/i18n/locales/zh-Hans.ts +1 -1
  52. package/src/i18n/locales/zh-Hant.po +102 -55
  53. package/src/i18n/locales/zh-Hant.ts +1 -1
  54. package/src/index.ts +5 -0
  55. package/src/lib/__tests__/theme-components.test.ts +107 -0
  56. package/src/lib/image.ts +3 -3
  57. package/src/lib/media-helpers.ts +54 -0
  58. package/src/lib/nav-reorder.ts +26 -0
  59. package/src/lib/navigation.ts +46 -0
  60. package/src/lib/theme-components.ts +76 -0
  61. package/src/routes/api/__tests__/posts.test.ts +8 -8
  62. package/src/routes/api/__tests__/timeline.test.ts +242 -0
  63. package/src/routes/api/timeline.tsx +145 -0
  64. package/src/routes/api/upload.ts +9 -5
  65. package/src/routes/dash/navigation.tsx +306 -0
  66. package/src/routes/pages/archive.tsx +15 -23
  67. package/src/routes/pages/collection.tsx +8 -15
  68. package/src/routes/pages/home.tsx +111 -122
  69. package/src/routes/pages/page.tsx +17 -30
  70. package/src/routes/pages/post.tsx +33 -42
  71. package/src/routes/pages/search.tsx +18 -22
  72. package/src/services/__tests__/media.test.ts +34 -7
  73. package/src/services/__tests__/navigation.test.ts +213 -0
  74. package/src/services/__tests__/post-timeline.test.ts +220 -0
  75. package/src/services/index.ts +7 -0
  76. package/src/services/media.ts +2 -1
  77. package/src/services/navigation.ts +165 -0
  78. package/src/services/post.ts +48 -1
  79. package/src/styles/components.css +59 -0
  80. package/src/theme/components/PostList.tsx +7 -0
  81. package/src/theme/components/index.ts +12 -0
  82. package/src/theme/components/timeline/ArticleCard.tsx +57 -0
  83. package/src/theme/components/timeline/ImageCard.tsx +80 -0
  84. package/src/theme/components/timeline/LinkCard.tsx +66 -0
  85. package/src/theme/components/timeline/NoteCard.tsx +41 -0
  86. package/src/theme/components/timeline/QuoteCard.tsx +55 -0
  87. package/src/theme/components/timeline/ThreadPreview.tsx +49 -0
  88. package/src/theme/components/timeline/TimelineFeed.tsx +52 -0
  89. package/src/theme/components/timeline/TimelineItem.tsx +39 -0
  90. package/src/theme/components/timeline/index.ts +8 -0
  91. package/src/theme/layouts/DashLayout.tsx +10 -0
  92. package/src/theme/layouts/SiteLayout.tsx +184 -0
  93. package/src/theme/layouts/index.ts +1 -0
  94. package/src/types/sortablejs.d.ts +23 -0
  95. package/src/types.ts +61 -0
  96. package/dist/app.d.ts +0 -38
  97. package/dist/app.d.ts.map +0 -1
  98. package/dist/auth.d.ts +0 -25
  99. package/dist/auth.d.ts.map +0 -1
  100. package/dist/db/index.d.ts +0 -10
  101. package/dist/db/index.d.ts.map +0 -1
  102. package/dist/db/schema.d.ts +0 -1543
  103. package/dist/db/schema.d.ts.map +0 -1
  104. package/dist/i18n/Trans.d.ts +0 -25
  105. package/dist/i18n/Trans.d.ts.map +0 -1
  106. package/dist/i18n/context.d.ts +0 -69
  107. package/dist/i18n/context.d.ts.map +0 -1
  108. package/dist/i18n/detect.d.ts +0 -20
  109. package/dist/i18n/detect.d.ts.map +0 -1
  110. package/dist/i18n/i18n.d.ts +0 -32
  111. package/dist/i18n/i18n.d.ts.map +0 -1
  112. package/dist/i18n/index.d.ts +0 -41
  113. package/dist/i18n/index.d.ts.map +0 -1
  114. package/dist/i18n/locales/en.d.ts +0 -3
  115. package/dist/i18n/locales/en.d.ts.map +0 -1
  116. package/dist/i18n/locales/zh-Hans.d.ts +0 -3
  117. package/dist/i18n/locales/zh-Hans.d.ts.map +0 -1
  118. package/dist/i18n/locales/zh-Hant.d.ts +0 -3
  119. package/dist/i18n/locales/zh-Hant.d.ts.map +0 -1
  120. package/dist/i18n/locales.d.ts +0 -11
  121. package/dist/i18n/locales.d.ts.map +0 -1
  122. package/dist/i18n/middleware.d.ts +0 -21
  123. package/dist/i18n/middleware.d.ts.map +0 -1
  124. package/dist/index.d.ts +0 -16
  125. package/dist/index.d.ts.map +0 -1
  126. package/dist/lib/config.d.ts +0 -83
  127. package/dist/lib/config.d.ts.map +0 -1
  128. package/dist/lib/constants.d.ts +0 -37
  129. package/dist/lib/constants.d.ts.map +0 -1
  130. package/dist/lib/image.d.ts +0 -73
  131. package/dist/lib/image.d.ts.map +0 -1
  132. package/dist/lib/index.d.ts +0 -9
  133. package/dist/lib/index.d.ts.map +0 -1
  134. package/dist/lib/markdown.d.ts +0 -60
  135. package/dist/lib/markdown.d.ts.map +0 -1
  136. package/dist/lib/schemas.d.ts +0 -130
  137. package/dist/lib/schemas.d.ts.map +0 -1
  138. package/dist/lib/sqid.d.ts +0 -60
  139. package/dist/lib/sqid.d.ts.map +0 -1
  140. package/dist/lib/sse.d.ts +0 -192
  141. package/dist/lib/sse.d.ts.map +0 -1
  142. package/dist/lib/theme.d.ts +0 -44
  143. package/dist/lib/theme.d.ts.map +0 -1
  144. package/dist/lib/time.d.ts +0 -90
  145. package/dist/lib/time.d.ts.map +0 -1
  146. package/dist/lib/url.d.ts +0 -82
  147. package/dist/lib/url.d.ts.map +0 -1
  148. package/dist/middleware/auth.d.ts +0 -24
  149. package/dist/middleware/auth.d.ts.map +0 -1
  150. package/dist/middleware/onboarding.d.ts +0 -26
  151. package/dist/middleware/onboarding.d.ts.map +0 -1
  152. package/dist/routes/api/posts.d.ts +0 -13
  153. package/dist/routes/api/posts.d.ts.map +0 -1
  154. package/dist/routes/api/search.d.ts +0 -13
  155. package/dist/routes/api/search.d.ts.map +0 -1
  156. package/dist/routes/api/upload.d.ts +0 -16
  157. package/dist/routes/api/upload.d.ts.map +0 -1
  158. package/dist/routes/dash/collections.d.ts +0 -13
  159. package/dist/routes/dash/collections.d.ts.map +0 -1
  160. package/dist/routes/dash/index.d.ts +0 -15
  161. package/dist/routes/dash/index.d.ts.map +0 -1
  162. package/dist/routes/dash/media.d.ts +0 -16
  163. package/dist/routes/dash/media.d.ts.map +0 -1
  164. package/dist/routes/dash/pages.d.ts +0 -15
  165. package/dist/routes/dash/pages.d.ts.map +0 -1
  166. package/dist/routes/dash/posts.d.ts +0 -13
  167. package/dist/routes/dash/posts.d.ts.map +0 -1
  168. package/dist/routes/dash/redirects.d.ts +0 -13
  169. package/dist/routes/dash/redirects.d.ts.map +0 -1
  170. package/dist/routes/dash/settings.d.ts +0 -15
  171. package/dist/routes/dash/settings.d.ts.map +0 -1
  172. package/dist/routes/feed/rss.d.ts +0 -13
  173. package/dist/routes/feed/rss.d.ts.map +0 -1
  174. package/dist/routes/feed/sitemap.d.ts +0 -13
  175. package/dist/routes/feed/sitemap.d.ts.map +0 -1
  176. package/dist/routes/pages/archive.d.ts +0 -15
  177. package/dist/routes/pages/archive.d.ts.map +0 -1
  178. package/dist/routes/pages/collection.d.ts +0 -13
  179. package/dist/routes/pages/collection.d.ts.map +0 -1
  180. package/dist/routes/pages/home.d.ts +0 -13
  181. package/dist/routes/pages/home.d.ts.map +0 -1
  182. package/dist/routes/pages/page.d.ts +0 -15
  183. package/dist/routes/pages/page.d.ts.map +0 -1
  184. package/dist/routes/pages/post.d.ts +0 -13
  185. package/dist/routes/pages/post.d.ts.map +0 -1
  186. package/dist/routes/pages/search.d.ts +0 -13
  187. package/dist/routes/pages/search.d.ts.map +0 -1
  188. package/dist/services/collection.d.ts +0 -32
  189. package/dist/services/collection.d.ts.map +0 -1
  190. package/dist/services/index.d.ts +0 -28
  191. package/dist/services/index.d.ts.map +0 -1
  192. package/dist/services/media.d.ts +0 -34
  193. package/dist/services/media.d.ts.map +0 -1
  194. package/dist/services/post.d.ts +0 -31
  195. package/dist/services/post.d.ts.map +0 -1
  196. package/dist/services/redirect.d.ts +0 -15
  197. package/dist/services/redirect.d.ts.map +0 -1
  198. package/dist/services/search.d.ts +0 -26
  199. package/dist/services/search.d.ts.map +0 -1
  200. package/dist/services/settings.d.ts +0 -18
  201. package/dist/services/settings.d.ts.map +0 -1
  202. package/dist/theme/color-themes.d.ts +0 -30
  203. package/dist/theme/color-themes.d.ts.map +0 -1
  204. package/dist/theme/components/ActionButtons.d.ts +0 -43
  205. package/dist/theme/components/ActionButtons.d.ts.map +0 -1
  206. package/dist/theme/components/CrudPageHeader.d.ts +0 -23
  207. package/dist/theme/components/CrudPageHeader.d.ts.map +0 -1
  208. package/dist/theme/components/DangerZone.d.ts +0 -36
  209. package/dist/theme/components/DangerZone.d.ts.map +0 -1
  210. package/dist/theme/components/EmptyState.d.ts +0 -27
  211. package/dist/theme/components/EmptyState.d.ts.map +0 -1
  212. package/dist/theme/components/ListItemRow.d.ts +0 -15
  213. package/dist/theme/components/ListItemRow.d.ts.map +0 -1
  214. package/dist/theme/components/MediaGallery.d.ts +0 -13
  215. package/dist/theme/components/MediaGallery.d.ts.map +0 -1
  216. package/dist/theme/components/PageForm.d.ts +0 -14
  217. package/dist/theme/components/PageForm.d.ts.map +0 -1
  218. package/dist/theme/components/Pagination.d.ts +0 -46
  219. package/dist/theme/components/Pagination.d.ts.map +0 -1
  220. package/dist/theme/components/PostForm.d.ts +0 -16
  221. package/dist/theme/components/PostForm.d.ts.map +0 -1
  222. package/dist/theme/components/PostList.d.ts +0 -10
  223. package/dist/theme/components/PostList.d.ts.map +0 -1
  224. package/dist/theme/components/ThreadView.d.ts +0 -15
  225. package/dist/theme/components/ThreadView.d.ts.map +0 -1
  226. package/dist/theme/components/TypeBadge.d.ts +0 -12
  227. package/dist/theme/components/TypeBadge.d.ts.map +0 -1
  228. package/dist/theme/components/VisibilityBadge.d.ts +0 -12
  229. package/dist/theme/components/VisibilityBadge.d.ts.map +0 -1
  230. package/dist/theme/components/index.d.ts +0 -14
  231. package/dist/theme/components/index.d.ts.map +0 -1
  232. package/dist/theme/index.d.ts +0 -21
  233. package/dist/theme/index.d.ts.map +0 -1
  234. package/dist/theme/layouts/BaseLayout.d.ts +0 -23
  235. package/dist/theme/layouts/BaseLayout.d.ts.map +0 -1
  236. package/dist/theme/layouts/DashLayout.d.ts +0 -17
  237. package/dist/theme/layouts/DashLayout.d.ts.map +0 -1
  238. package/dist/theme/layouts/index.d.ts +0 -3
  239. package/dist/theme/layouts/index.d.ts.map +0 -1
  240. package/dist/types.d.ts +0 -237
  241. package/dist/types.d.ts.map +0 -1
@@ -1,47 +1,25 @@
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
  * Custom Page Route
5
4
  *
6
5
  * Catch-all route for custom pages accessible via their path field
7
6
  */ import { Hono } from "hono";
8
- import { useLingui as $_useLingui } from "@jant/core/i18n";
9
- import { BaseLayout } from "../../theme/layouts/index.js";
7
+ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
8
+ import { getNavigationData } from "../../lib/navigation.js";
10
9
  export const pageRoutes = new Hono();
11
10
  function PageContent({ page }) {
12
- const { i18n: $__i18n, _: $__ } = $_useLingui();
13
- return /*#__PURE__*/ _jsxs("div", {
14
- class: "container py-8 max-w-2xl",
11
+ return /*#__PURE__*/ _jsxs("article", {
12
+ class: "h-entry",
15
13
  children: [
16
- /*#__PURE__*/ _jsxs("article", {
17
- class: "h-entry",
18
- children: [
19
- page.title && /*#__PURE__*/ _jsx("h1", {
20
- class: "p-name text-3xl font-semibold mb-6",
21
- children: page.title
22
- }),
23
- /*#__PURE__*/ _jsx("div", {
24
- class: "e-content prose",
25
- dangerouslySetInnerHTML: {
26
- __html: page.contentHtml || ""
27
- }
28
- })
29
- ]
14
+ page.title && /*#__PURE__*/ _jsx("h1", {
15
+ class: "p-name text-3xl font-semibold mb-6",
16
+ children: page.title
30
17
  }),
31
- /*#__PURE__*/ _jsx("nav", {
32
- class: "mt-8 pt-6 border-t",
33
- children: /*#__PURE__*/ _jsxs("a", {
34
- href: "/",
35
- class: "text-sm hover:underline",
36
- children: [
37
- "←",
38
- " ",
39
- $__i18n._({
40
- id: "x4RuFo",
41
- message: "Back to home"
42
- })
43
- ]
44
- })
18
+ /*#__PURE__*/ _jsx("div", {
19
+ class: "e-content prose",
20
+ dangerouslySetInnerHTML: {
21
+ __html: page.contentHtml || ""
22
+ }
45
23
  })
46
24
  ]
47
25
  });
@@ -59,13 +37,16 @@ pageRoutes.get("/:path", async (c)=>{
59
37
  if (page.visibility === "draft") {
60
38
  return c.notFound();
61
39
  }
62
- const siteName = await getSiteName(c);
40
+ const navData = await getNavigationData(c);
63
41
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
64
- title: `${page.title} - ${siteName}`,
42
+ title: `${page.title} - ${navData.siteName}`,
65
43
  description: page.content?.slice(0, 160),
66
44
  c: c,
67
- children: /*#__PURE__*/ _jsx(PageContent, {
68
- page: page
45
+ children: /*#__PURE__*/ _jsx(SiteLayout, {
46
+ ...navData,
47
+ children: /*#__PURE__*/ _jsx(PageContent, {
48
+ page: page
49
+ })
69
50
  })
70
51
  }));
71
52
  });
@@ -1,66 +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";
8
7
  import { MediaGallery } from "../../theme/components/index.js";
9
8
  import * as sqid from "../../lib/sqid.js";
10
9
  import * as time from "../../lib/time.js";
11
10
  import { getMediaUrl, getImageUrl } from "../../lib/image.js";
11
+ import { getNavigationData } from "../../lib/navigation.js";
12
12
  export const postRoutes = new Hono();
13
13
  function PostContent({ post, mediaAttachments }) {
14
14
  const { i18n: $__i18n, _: $__ } = $_useLingui();
15
- return /*#__PURE__*/ _jsxs("div", {
16
- class: "container py-8",
15
+ return /*#__PURE__*/ _jsxs("article", {
16
+ class: "h-entry",
17
17
  children: [
18
- /*#__PURE__*/ _jsxs("article", {
19
- 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",
20
33
  children: [
21
- post.title && /*#__PURE__*/ _jsx("h1", {
22
- class: "p-name text-2xl font-semibold mb-4",
23
- children: post.title
24
- }),
25
- /*#__PURE__*/ _jsx("div", {
26
- class: "e-content prose",
27
- dangerouslySetInnerHTML: {
28
- __html: post.contentHtml || ""
29
- }
34
+ /*#__PURE__*/ _jsx("time", {
35
+ class: "dt-published",
36
+ datetime: time.toISOString(post.publishedAt),
37
+ children: time.formatDate(post.publishedAt)
30
38
  }),
31
- mediaAttachments.length > 0 && /*#__PURE__*/ _jsx(MediaGallery, {
32
- attachments: mediaAttachments
33
- }),
34
- /*#__PURE__*/ _jsxs("footer", {
35
- class: "mt-6 pt-4 border-t text-sm text-muted-foreground",
36
- children: [
37
- /*#__PURE__*/ _jsx("time", {
38
- class: "dt-published",
39
- datetime: time.toISOString(post.publishedAt),
40
- children: time.formatDate(post.publishedAt)
41
- }),
42
- /*#__PURE__*/ _jsx("a", {
43
- href: `/p/${sqid.encode(post.id)}`,
44
- class: "u-url ml-4",
45
- children: $__i18n._({
46
- id: "D9Oea+",
47
- message: "Permalink"
48
- })
49
- })
50
- ]
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
+ })
51
46
  })
52
47
  ]
53
- }),
54
- /*#__PURE__*/ _jsx("nav", {
55
- class: "mt-8",
56
- children: /*#__PURE__*/ _jsx("a", {
57
- href: "/",
58
- class: "text-sm hover:underline",
59
- children: $__i18n._({
60
- id: "biOepV",
61
- message: "← Back to home"
62
- })
63
- })
64
48
  })
65
49
  ]
66
50
  });
@@ -103,15 +87,18 @@ postRoutes.get("/:id", async (c)=>{
103
87
  position: m.position,
104
88
  mimeType: m.mimeType
105
89
  }));
106
- const siteName = await getSiteName(c);
107
- const title = post.title || siteName;
90
+ const navData = await getNavigationData(c);
91
+ const title = post.title || navData.siteName;
108
92
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
109
93
  title: title,
110
94
  description: post.content?.slice(0, 160),
111
95
  c: c,
112
- children: /*#__PURE__*/ _jsx(PostContent, {
113
- post: post,
114
- mediaAttachments: mediaAttachments
96
+ children: /*#__PURE__*/ _jsx(SiteLayout, {
97
+ ...navData,
98
+ children: /*#__PURE__*/ _jsx(PostContent, {
99
+ post: post,
100
+ mediaAttachments: mediaAttachments
101
+ })
115
102
  })
116
103
  }));
117
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
  });
@@ -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
  }
@@ -65,7 +65,7 @@ export function createMediaService(db) {
65
65
  return rows.map(toMedia);
66
66
  },
67
67
  async create (data) {
68
- const id = uuidv7();
68
+ const id = data.id ?? uuidv7();
69
69
  const timestamp = now();
70
70
  const result = await db.insert(media).values({
71
71
  id,
@@ -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
  }
@@ -37,6 +37,11 @@ export const PostList = ({ posts })=>{
37
37
  viewLabel: $__i18n._({
38
38
  id: "jpctdh",
39
39
  message: "View"
40
+ }),
41
+ deleteAction: `/dash/posts/${sqid.encode(post.id)}/delete`,
42
+ deleteConfirm: $__i18n._({
43
+ id: "KmGXnO",
44
+ message: "Are you sure you want to delete this post? This cannot be undone."
40
45
  })
41
46
  }),
42
47
  children: [
@@ -11,3 +11,5 @@ export { PostList } from "./PostList.js";
11
11
  export { ThreadView } from "./ThreadView.js";
12
12
  export { TypeBadge } from "./TypeBadge.js";
13
13
  export { VisibilityBadge } from "./VisibilityBadge.js";
14
+ // Timeline components
15
+ export { NoteCard, ArticleCard, LinkCard, QuoteCard, ImageCard, ThreadPreview, TimelineItem, TimelineFeed } from "./timeline/index.js";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Article Card Component
3
+ *
4
+ * Prominent title + excerpt for type="article" posts.
5
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
+ import * as sqid from "../../../lib/sqid.js";
7
+ import * as time from "../../../lib/time.js";
8
+ export const ArticleCard = ({ post, compact })=>{
9
+ const permalink = `/p/${sqid.encode(post.id)}`;
10
+ const excerpt = post.content ? post.content.length > 160 ? post.content.slice(0, 160) + "..." : post.content : null;
11
+ return /*#__PURE__*/ _jsxs("article", {
12
+ class: `h-entry timeline-card${compact ? " timeline-card-compact" : ""}`,
13
+ children: [
14
+ post.title && /*#__PURE__*/ _jsx("h2", {
15
+ class: `p-name font-semibold ${compact ? "text-sm" : "text-lg"} mb-1`,
16
+ children: /*#__PURE__*/ _jsx("a", {
17
+ href: permalink,
18
+ class: "u-url hover:underline",
19
+ children: post.title
20
+ })
21
+ }),
22
+ !compact && excerpt && /*#__PURE__*/ _jsx("p", {
23
+ class: "e-content text-sm text-muted-foreground line-clamp-3",
24
+ children: excerpt
25
+ }),
26
+ /*#__PURE__*/ _jsxs("footer", {
27
+ class: "mt-2 text-xs text-muted-foreground",
28
+ children: [
29
+ /*#__PURE__*/ _jsx("a", {
30
+ href: permalink,
31
+ class: "u-url hover:underline",
32
+ children: /*#__PURE__*/ _jsx("time", {
33
+ class: "dt-published",
34
+ datetime: time.toISOString(post.publishedAt),
35
+ children: time.formatDate(post.publishedAt)
36
+ })
37
+ }),
38
+ !compact && /*#__PURE__*/ _jsx("span", {
39
+ class: "ml-2",
40
+ children: /*#__PURE__*/ _jsx("a", {
41
+ href: permalink,
42
+ class: "hover:underline",
43
+ children: "Read more →"
44
+ })
45
+ })
46
+ ]
47
+ })
48
+ ]
49
+ });
50
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Image Card Component
3
+ *
4
+ * Image-first layout for type="image" posts.
5
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
+ import { MediaGallery } from "../MediaGallery.js";
7
+ import * as sqid from "../../../lib/sqid.js";
8
+ import * as time from "../../../lib/time.js";
9
+ export const ImageCard = ({ post, compact })=>{
10
+ const permalink = `/p/${sqid.encode(post.id)}`;
11
+ if (compact) {
12
+ return /*#__PURE__*/ _jsxs("article", {
13
+ class: "h-entry timeline-card timeline-card-compact",
14
+ children: [
15
+ post.title && /*#__PURE__*/ _jsx("h2", {
16
+ class: "p-name text-sm font-medium mb-1",
17
+ children: /*#__PURE__*/ _jsx("a", {
18
+ href: permalink,
19
+ class: "u-url hover:underline",
20
+ children: post.title
21
+ })
22
+ }),
23
+ post.contentHtml && /*#__PURE__*/ _jsx("div", {
24
+ class: "e-content prose prose-sm text-muted-foreground",
25
+ dangerouslySetInnerHTML: {
26
+ __html: post.contentHtml
27
+ }
28
+ }),
29
+ /*#__PURE__*/ _jsx("footer", {
30
+ class: "mt-1 text-xs text-muted-foreground",
31
+ children: /*#__PURE__*/ _jsx("a", {
32
+ href: permalink,
33
+ class: "u-url hover:underline",
34
+ children: /*#__PURE__*/ _jsx("time", {
35
+ class: "dt-published",
36
+ datetime: time.toISOString(post.publishedAt),
37
+ children: time.formatDate(post.publishedAt)
38
+ })
39
+ })
40
+ })
41
+ ]
42
+ });
43
+ }
44
+ return /*#__PURE__*/ _jsxs("article", {
45
+ class: "h-entry timeline-card timeline-card-image",
46
+ children: [
47
+ post.mediaAttachments.length > 0 && /*#__PURE__*/ _jsx("div", {
48
+ class: "timeline-card-image-gallery",
49
+ children: /*#__PURE__*/ _jsx(MediaGallery, {
50
+ attachments: post.mediaAttachments
51
+ })
52
+ }),
53
+ /*#__PURE__*/ _jsxs("div", {
54
+ class: "p-4",
55
+ children: [
56
+ post.title && /*#__PURE__*/ _jsx("h2", {
57
+ class: "p-name font-medium mb-1",
58
+ children: /*#__PURE__*/ _jsx("a", {
59
+ href: permalink,
60
+ class: "u-url hover:underline",
61
+ children: post.title
62
+ })
63
+ }),
64
+ post.contentHtml && /*#__PURE__*/ _jsx("div", {
65
+ class: "e-content prose prose-sm",
66
+ dangerouslySetInnerHTML: {
67
+ __html: post.contentHtml
68
+ }
69
+ }),
70
+ /*#__PURE__*/ _jsx("footer", {
71
+ class: "mt-2 text-xs text-muted-foreground",
72
+ children: /*#__PURE__*/ _jsx("a", {
73
+ href: permalink,
74
+ class: "u-url hover:underline",
75
+ children: /*#__PURE__*/ _jsx("time", {
76
+ class: "dt-published",
77
+ datetime: time.toISOString(post.publishedAt),
78
+ children: time.formatDate(post.publishedAt)
79
+ })
80
+ })
81
+ })
82
+ ]
83
+ })
84
+ ]
85
+ });
86
+ };