@jant/core 0.3.7 → 0.3.9

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 (258) hide show
  1. package/dist/app.js +11 -4
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +15 -1
  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 +39 -15
  8. package/dist/lib/media-helpers.js +49 -0
  9. package/dist/lib/nav-reorder.js +27 -0
  10. package/dist/lib/navigation.js +35 -0
  11. package/dist/lib/storage.js +164 -0
  12. package/dist/lib/theme-components.js +49 -0
  13. package/dist/routes/api/posts.js +12 -7
  14. package/dist/routes/api/timeline.js +116 -0
  15. package/dist/routes/api/upload.js +35 -24
  16. package/dist/routes/dash/media.js +24 -14
  17. package/dist/routes/dash/navigation.js +274 -0
  18. package/dist/routes/dash/posts.js +4 -1
  19. package/dist/routes/feed/rss.js +3 -2
  20. package/dist/routes/pages/archive.js +14 -27
  21. package/dist/routes/pages/collection.js +10 -19
  22. package/dist/routes/pages/home.js +84 -126
  23. package/dist/routes/pages/page.js +19 -38
  24. package/dist/routes/pages/post.js +47 -56
  25. package/dist/routes/pages/search.js +13 -26
  26. package/dist/services/index.js +3 -1
  27. package/dist/services/media.js +8 -6
  28. package/dist/services/navigation.js +115 -0
  29. package/dist/services/post.js +26 -1
  30. package/dist/theme/components/PostForm.js +4 -3
  31. package/dist/theme/components/PostList.js +5 -0
  32. package/dist/theme/components/index.js +2 -0
  33. package/dist/theme/components/timeline/ArticleCard.js +50 -0
  34. package/dist/theme/components/timeline/ImageCard.js +86 -0
  35. package/dist/theme/components/timeline/LinkCard.js +62 -0
  36. package/dist/theme/components/timeline/NoteCard.js +37 -0
  37. package/dist/theme/components/timeline/QuoteCard.js +51 -0
  38. package/dist/theme/components/timeline/ThreadPreview.js +52 -0
  39. package/dist/theme/components/timeline/TimelineFeed.js +43 -0
  40. package/dist/theme/components/timeline/TimelineItem.js +25 -0
  41. package/dist/theme/components/timeline/index.js +8 -0
  42. package/dist/theme/layouts/DashLayout.js +8 -0
  43. package/dist/theme/layouts/SiteLayout.js +160 -0
  44. package/dist/theme/layouts/index.js +1 -0
  45. package/dist/types/sortablejs.d.js +5 -0
  46. package/dist/types.js +32 -0
  47. package/package.json +4 -2
  48. package/src/__tests__/helpers/app.ts +1 -0
  49. package/src/__tests__/helpers/db.ts +20 -0
  50. package/src/app.tsx +12 -7
  51. package/src/client.ts +1 -0
  52. package/src/db/migrations/0003_add_navigation_links.sql +8 -0
  53. package/src/db/migrations/0004_add_storage_provider.sql +3 -0
  54. package/src/db/migrations/meta/0003_snapshot.json +821 -0
  55. package/src/db/migrations/meta/_journal.json +21 -0
  56. package/src/db/schema.ts +15 -1
  57. package/src/i18n/locales/en.po +148 -80
  58. package/src/i18n/locales/en.ts +1 -1
  59. package/src/i18n/locales/zh-Hans.po +150 -103
  60. package/src/i18n/locales/zh-Hans.ts +1 -1
  61. package/src/i18n/locales/zh-Hant.po +150 -103
  62. package/src/i18n/locales/zh-Hant.ts +1 -1
  63. package/src/index.ts +5 -0
  64. package/src/lib/__tests__/image.test.ts +96 -0
  65. package/src/lib/__tests__/storage.test.ts +162 -0
  66. package/src/lib/__tests__/theme-components.test.ts +107 -0
  67. package/src/lib/image.ts +46 -16
  68. package/src/lib/media-helpers.ts +65 -0
  69. package/src/lib/nav-reorder.ts +26 -0
  70. package/src/lib/navigation.ts +46 -0
  71. package/src/lib/storage.ts +236 -0
  72. package/src/lib/theme-components.ts +76 -0
  73. package/src/routes/api/__tests__/posts.test.ts +8 -8
  74. package/src/routes/api/__tests__/timeline.test.ts +242 -0
  75. package/src/routes/api/posts.ts +20 -6
  76. package/src/routes/api/timeline.tsx +152 -0
  77. package/src/routes/api/upload.ts +52 -25
  78. package/src/routes/dash/media.tsx +40 -8
  79. package/src/routes/dash/navigation.tsx +306 -0
  80. package/src/routes/dash/posts.tsx +5 -0
  81. package/src/routes/feed/rss.ts +3 -2
  82. package/src/routes/pages/archive.tsx +15 -23
  83. package/src/routes/pages/collection.tsx +8 -15
  84. package/src/routes/pages/home.tsx +118 -122
  85. package/src/routes/pages/page.tsx +17 -30
  86. package/src/routes/pages/post.tsx +63 -60
  87. package/src/routes/pages/search.tsx +18 -22
  88. package/src/services/__tests__/media.test.ts +73 -28
  89. package/src/services/__tests__/navigation.test.ts +213 -0
  90. package/src/services/__tests__/post-timeline.test.ts +220 -0
  91. package/src/services/index.ts +7 -0
  92. package/src/services/media.ts +12 -8
  93. package/src/services/navigation.ts +165 -0
  94. package/src/services/post.ts +48 -1
  95. package/src/styles/components.css +59 -0
  96. package/src/theme/components/PostForm.tsx +13 -2
  97. package/src/theme/components/PostList.tsx +7 -0
  98. package/src/theme/components/index.ts +12 -0
  99. package/src/theme/components/timeline/ArticleCard.tsx +57 -0
  100. package/src/theme/components/timeline/ImageCard.tsx +80 -0
  101. package/src/theme/components/timeline/LinkCard.tsx +66 -0
  102. package/src/theme/components/timeline/NoteCard.tsx +41 -0
  103. package/src/theme/components/timeline/QuoteCard.tsx +55 -0
  104. package/src/theme/components/timeline/ThreadPreview.tsx +49 -0
  105. package/src/theme/components/timeline/TimelineFeed.tsx +52 -0
  106. package/src/theme/components/timeline/TimelineItem.tsx +39 -0
  107. package/src/theme/components/timeline/index.ts +8 -0
  108. package/src/theme/layouts/DashLayout.tsx +10 -0
  109. package/src/theme/layouts/SiteLayout.tsx +184 -0
  110. package/src/theme/layouts/index.ts +1 -0
  111. package/src/types/sortablejs.d.ts +23 -0
  112. package/src/types.ts +102 -1
  113. package/dist/app.d.ts +0 -38
  114. package/dist/app.d.ts.map +0 -1
  115. package/dist/auth.d.ts +0 -25
  116. package/dist/auth.d.ts.map +0 -1
  117. package/dist/db/index.d.ts +0 -10
  118. package/dist/db/index.d.ts.map +0 -1
  119. package/dist/db/schema.d.ts +0 -1543
  120. package/dist/db/schema.d.ts.map +0 -1
  121. package/dist/i18n/Trans.d.ts +0 -25
  122. package/dist/i18n/Trans.d.ts.map +0 -1
  123. package/dist/i18n/context.d.ts +0 -69
  124. package/dist/i18n/context.d.ts.map +0 -1
  125. package/dist/i18n/detect.d.ts +0 -20
  126. package/dist/i18n/detect.d.ts.map +0 -1
  127. package/dist/i18n/i18n.d.ts +0 -32
  128. package/dist/i18n/i18n.d.ts.map +0 -1
  129. package/dist/i18n/index.d.ts +0 -41
  130. package/dist/i18n/index.d.ts.map +0 -1
  131. package/dist/i18n/locales/en.d.ts +0 -3
  132. package/dist/i18n/locales/en.d.ts.map +0 -1
  133. package/dist/i18n/locales/zh-Hans.d.ts +0 -3
  134. package/dist/i18n/locales/zh-Hans.d.ts.map +0 -1
  135. package/dist/i18n/locales/zh-Hant.d.ts +0 -3
  136. package/dist/i18n/locales/zh-Hant.d.ts.map +0 -1
  137. package/dist/i18n/locales.d.ts +0 -11
  138. package/dist/i18n/locales.d.ts.map +0 -1
  139. package/dist/i18n/middleware.d.ts +0 -21
  140. package/dist/i18n/middleware.d.ts.map +0 -1
  141. package/dist/index.d.ts +0 -16
  142. package/dist/index.d.ts.map +0 -1
  143. package/dist/lib/config.d.ts +0 -83
  144. package/dist/lib/config.d.ts.map +0 -1
  145. package/dist/lib/constants.d.ts +0 -37
  146. package/dist/lib/constants.d.ts.map +0 -1
  147. package/dist/lib/image.d.ts +0 -73
  148. package/dist/lib/image.d.ts.map +0 -1
  149. package/dist/lib/index.d.ts +0 -9
  150. package/dist/lib/index.d.ts.map +0 -1
  151. package/dist/lib/markdown.d.ts +0 -60
  152. package/dist/lib/markdown.d.ts.map +0 -1
  153. package/dist/lib/schemas.d.ts +0 -130
  154. package/dist/lib/schemas.d.ts.map +0 -1
  155. package/dist/lib/sqid.d.ts +0 -60
  156. package/dist/lib/sqid.d.ts.map +0 -1
  157. package/dist/lib/sse.d.ts +0 -192
  158. package/dist/lib/sse.d.ts.map +0 -1
  159. package/dist/lib/theme.d.ts +0 -44
  160. package/dist/lib/theme.d.ts.map +0 -1
  161. package/dist/lib/time.d.ts +0 -90
  162. package/dist/lib/time.d.ts.map +0 -1
  163. package/dist/lib/url.d.ts +0 -82
  164. package/dist/lib/url.d.ts.map +0 -1
  165. package/dist/middleware/auth.d.ts +0 -24
  166. package/dist/middleware/auth.d.ts.map +0 -1
  167. package/dist/middleware/onboarding.d.ts +0 -26
  168. package/dist/middleware/onboarding.d.ts.map +0 -1
  169. package/dist/routes/api/posts.d.ts +0 -13
  170. package/dist/routes/api/posts.d.ts.map +0 -1
  171. package/dist/routes/api/search.d.ts +0 -13
  172. package/dist/routes/api/search.d.ts.map +0 -1
  173. package/dist/routes/api/upload.d.ts +0 -16
  174. package/dist/routes/api/upload.d.ts.map +0 -1
  175. package/dist/routes/dash/collections.d.ts +0 -13
  176. package/dist/routes/dash/collections.d.ts.map +0 -1
  177. package/dist/routes/dash/index.d.ts +0 -15
  178. package/dist/routes/dash/index.d.ts.map +0 -1
  179. package/dist/routes/dash/media.d.ts +0 -16
  180. package/dist/routes/dash/media.d.ts.map +0 -1
  181. package/dist/routes/dash/pages.d.ts +0 -15
  182. package/dist/routes/dash/pages.d.ts.map +0 -1
  183. package/dist/routes/dash/posts.d.ts +0 -13
  184. package/dist/routes/dash/posts.d.ts.map +0 -1
  185. package/dist/routes/dash/redirects.d.ts +0 -13
  186. package/dist/routes/dash/redirects.d.ts.map +0 -1
  187. package/dist/routes/dash/settings.d.ts +0 -15
  188. package/dist/routes/dash/settings.d.ts.map +0 -1
  189. package/dist/routes/feed/rss.d.ts +0 -13
  190. package/dist/routes/feed/rss.d.ts.map +0 -1
  191. package/dist/routes/feed/sitemap.d.ts +0 -13
  192. package/dist/routes/feed/sitemap.d.ts.map +0 -1
  193. package/dist/routes/pages/archive.d.ts +0 -15
  194. package/dist/routes/pages/archive.d.ts.map +0 -1
  195. package/dist/routes/pages/collection.d.ts +0 -13
  196. package/dist/routes/pages/collection.d.ts.map +0 -1
  197. package/dist/routes/pages/home.d.ts +0 -13
  198. package/dist/routes/pages/home.d.ts.map +0 -1
  199. package/dist/routes/pages/page.d.ts +0 -15
  200. package/dist/routes/pages/page.d.ts.map +0 -1
  201. package/dist/routes/pages/post.d.ts +0 -13
  202. package/dist/routes/pages/post.d.ts.map +0 -1
  203. package/dist/routes/pages/search.d.ts +0 -13
  204. package/dist/routes/pages/search.d.ts.map +0 -1
  205. package/dist/services/collection.d.ts +0 -32
  206. package/dist/services/collection.d.ts.map +0 -1
  207. package/dist/services/index.d.ts +0 -28
  208. package/dist/services/index.d.ts.map +0 -1
  209. package/dist/services/media.d.ts +0 -34
  210. package/dist/services/media.d.ts.map +0 -1
  211. package/dist/services/post.d.ts +0 -31
  212. package/dist/services/post.d.ts.map +0 -1
  213. package/dist/services/redirect.d.ts +0 -15
  214. package/dist/services/redirect.d.ts.map +0 -1
  215. package/dist/services/search.d.ts +0 -26
  216. package/dist/services/search.d.ts.map +0 -1
  217. package/dist/services/settings.d.ts +0 -18
  218. package/dist/services/settings.d.ts.map +0 -1
  219. package/dist/theme/color-themes.d.ts +0 -30
  220. package/dist/theme/color-themes.d.ts.map +0 -1
  221. package/dist/theme/components/ActionButtons.d.ts +0 -43
  222. package/dist/theme/components/ActionButtons.d.ts.map +0 -1
  223. package/dist/theme/components/CrudPageHeader.d.ts +0 -23
  224. package/dist/theme/components/CrudPageHeader.d.ts.map +0 -1
  225. package/dist/theme/components/DangerZone.d.ts +0 -36
  226. package/dist/theme/components/DangerZone.d.ts.map +0 -1
  227. package/dist/theme/components/EmptyState.d.ts +0 -27
  228. package/dist/theme/components/EmptyState.d.ts.map +0 -1
  229. package/dist/theme/components/ListItemRow.d.ts +0 -15
  230. package/dist/theme/components/ListItemRow.d.ts.map +0 -1
  231. package/dist/theme/components/MediaGallery.d.ts +0 -13
  232. package/dist/theme/components/MediaGallery.d.ts.map +0 -1
  233. package/dist/theme/components/PageForm.d.ts +0 -14
  234. package/dist/theme/components/PageForm.d.ts.map +0 -1
  235. package/dist/theme/components/Pagination.d.ts +0 -46
  236. package/dist/theme/components/Pagination.d.ts.map +0 -1
  237. package/dist/theme/components/PostForm.d.ts +0 -16
  238. package/dist/theme/components/PostForm.d.ts.map +0 -1
  239. package/dist/theme/components/PostList.d.ts +0 -10
  240. package/dist/theme/components/PostList.d.ts.map +0 -1
  241. package/dist/theme/components/ThreadView.d.ts +0 -15
  242. package/dist/theme/components/ThreadView.d.ts.map +0 -1
  243. package/dist/theme/components/TypeBadge.d.ts +0 -12
  244. package/dist/theme/components/TypeBadge.d.ts.map +0 -1
  245. package/dist/theme/components/VisibilityBadge.d.ts +0 -12
  246. package/dist/theme/components/VisibilityBadge.d.ts.map +0 -1
  247. package/dist/theme/components/index.d.ts +0 -14
  248. package/dist/theme/components/index.d.ts.map +0 -1
  249. package/dist/theme/index.d.ts +0 -21
  250. package/dist/theme/index.d.ts.map +0 -1
  251. package/dist/theme/layouts/BaseLayout.d.ts +0 -23
  252. package/dist/theme/layouts/BaseLayout.d.ts.map +0 -1
  253. package/dist/theme/layouts/DashLayout.d.ts +0 -17
  254. package/dist/theme/layouts/DashLayout.d.ts.map +0 -1
  255. package/dist/theme/layouts/index.d.ts +0 -3
  256. package/dist/theme/layouts/index.d.ts.map +0 -1
  257. package/dist/types.d.ts +0 -237
  258. package/dist/types.d.ts.map +0 -1
@@ -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
  }
@@ -2,8 +2,8 @@
2
2
  * Post Creation/Edit Form
3
3
  */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
4
4
  import { useLingui as $_useLingui } from "@jant/core/i18n";
5
- import { getMediaUrl, getImageUrl } from "../../lib/image.js";
6
- export const PostForm = ({ post, action, mediaAttachments, r2PublicUrl, imageTransformUrl, collections, postCollectionIds })=>{
5
+ import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "../../lib/image.js";
6
+ export const PostForm = ({ post, action, mediaAttachments, r2PublicUrl, imageTransformUrl, s3PublicUrl, collections, postCollectionIds })=>{
7
7
  const { i18n: $__i18n, _: $__ } = $_useLingui();
8
8
  const isEdit = !!post;
9
9
  const existingMediaIds = (mediaAttachments ?? []).map((m)=>m.id);
@@ -150,7 +150,8 @@ export const PostForm = ({ post, action, mediaAttachments, r2PublicUrl, imageTra
150
150
  mediaAttachments && mediaAttachments.length > 0 && /*#__PURE__*/ _jsx("div", {
151
151
  class: "grid grid-cols-4 sm:grid-cols-6 gap-2 mb-2",
152
152
  children: mediaAttachments.map((m)=>{
153
- const url = getMediaUrl(m.id, m.r2Key, r2PublicUrl);
153
+ const pUrl = getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl);
154
+ const url = getMediaUrl(m.id, m.storageKey, pUrl);
154
155
  const thumbUrl = getImageUrl(url, imageTransformUrl, {
155
156
  width: 150,
156
157
  quality: 80,
@@ -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
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Link Card Component
3
+ *
4
+ * External link emphasis for type="link" 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 LinkCard = ({ post, compact })=>{
9
+ const permalink = `/p/${sqid.encode(post.id)}`;
10
+ return /*#__PURE__*/ _jsxs("article", {
11
+ class: `h-entry timeline-card timeline-card-link${compact ? " timeline-card-compact" : ""}`,
12
+ children: [
13
+ post.sourceDomain && /*#__PURE__*/ _jsxs("div", {
14
+ class: "text-xs text-muted-foreground mb-1 flex items-center gap-1",
15
+ children: [
16
+ /*#__PURE__*/ _jsx("svg", {
17
+ class: "size-3",
18
+ xmlns: "http://www.w3.org/2000/svg",
19
+ fill: "none",
20
+ viewBox: "0 0 24 24",
21
+ "stroke-width": "2",
22
+ stroke: "currentColor",
23
+ children: /*#__PURE__*/ _jsx("path", {
24
+ d: "M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
25
+ })
26
+ }),
27
+ /*#__PURE__*/ _jsx("span", {
28
+ children: post.sourceDomain
29
+ })
30
+ ]
31
+ }),
32
+ post.title && /*#__PURE__*/ _jsx("h2", {
33
+ class: `p-name font-semibold ${compact ? "text-sm" : "text-base"} mb-1`,
34
+ children: /*#__PURE__*/ _jsx("a", {
35
+ href: post.sourceUrl || permalink,
36
+ class: "u-url hover:underline",
37
+ target: post.sourceUrl ? "_blank" : undefined,
38
+ rel: post.sourceUrl ? "noopener noreferrer" : undefined,
39
+ children: post.title
40
+ })
41
+ }),
42
+ !compact && post.contentHtml && /*#__PURE__*/ _jsx("div", {
43
+ class: "e-content prose prose-sm text-muted-foreground",
44
+ dangerouslySetInnerHTML: {
45
+ __html: post.contentHtml
46
+ }
47
+ }),
48
+ /*#__PURE__*/ _jsx("footer", {
49
+ class: "mt-2 text-xs text-muted-foreground",
50
+ children: /*#__PURE__*/ _jsx("a", {
51
+ href: permalink,
52
+ class: "hover:underline",
53
+ children: /*#__PURE__*/ _jsx("time", {
54
+ class: "dt-published",
55
+ datetime: time.toISOString(post.publishedAt),
56
+ children: time.formatDate(post.publishedAt)
57
+ })
58
+ })
59
+ })
60
+ ]
61
+ });
62
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Note Card Component
3
+ *
4
+ * Text-first, minimal card for type="note" 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 NoteCard = ({ post, compact })=>{
10
+ const permalink = `/p/${sqid.encode(post.id)}`;
11
+ return /*#__PURE__*/ _jsxs("article", {
12
+ class: `h-entry timeline-card${compact ? " timeline-card-compact" : ""}`,
13
+ children: [
14
+ post.contentHtml && /*#__PURE__*/ _jsx("div", {
15
+ class: `e-content prose ${compact ? "prose-sm" : "prose-sm"}`,
16
+ dangerouslySetInnerHTML: {
17
+ __html: post.contentHtml
18
+ }
19
+ }),
20
+ !compact && post.mediaAttachments.length > 0 && /*#__PURE__*/ _jsx(MediaGallery, {
21
+ attachments: post.mediaAttachments
22
+ }),
23
+ /*#__PURE__*/ _jsx("footer", {
24
+ class: "mt-2 text-xs text-muted-foreground",
25
+ children: /*#__PURE__*/ _jsx("a", {
26
+ href: permalink,
27
+ class: "u-url hover:underline",
28
+ children: /*#__PURE__*/ _jsx("time", {
29
+ class: "dt-published",
30
+ datetime: time.toISOString(post.publishedAt),
31
+ children: time.formatDate(post.publishedAt)
32
+ })
33
+ })
34
+ })
35
+ ]
36
+ });
37
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Quote Card Component
3
+ *
4
+ * Blockquote + attribution for type="quote" 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 QuoteCard = ({ post, compact })=>{
9
+ const permalink = `/p/${sqid.encode(post.id)}`;
10
+ return /*#__PURE__*/ _jsxs("article", {
11
+ class: `h-entry timeline-card timeline-card-quote${compact ? " timeline-card-compact" : ""}`,
12
+ children: [
13
+ post.contentHtml && /*#__PURE__*/ _jsx("blockquote", {
14
+ class: `e-content italic ${compact ? "text-sm" : "text-base"} leading-relaxed`,
15
+ children: /*#__PURE__*/ _jsx("div", {
16
+ dangerouslySetInnerHTML: {
17
+ __html: post.contentHtml
18
+ }
19
+ })
20
+ }),
21
+ !compact && (post.sourceName || post.sourceUrl) && /*#__PURE__*/ _jsxs("div", {
22
+ class: "mt-2 text-sm text-muted-foreground",
23
+ children: [
24
+ "—",
25
+ " ",
26
+ post.sourceUrl ? /*#__PURE__*/ _jsx("a", {
27
+ href: post.sourceUrl,
28
+ class: "hover:underline",
29
+ target: "_blank",
30
+ rel: "noopener noreferrer",
31
+ children: post.sourceName || post.sourceDomain || "Source"
32
+ }) : /*#__PURE__*/ _jsx("span", {
33
+ children: post.sourceName
34
+ })
35
+ ]
36
+ }),
37
+ /*#__PURE__*/ _jsx("footer", {
38
+ class: "mt-2 text-xs text-muted-foreground",
39
+ children: /*#__PURE__*/ _jsx("a", {
40
+ href: permalink,
41
+ class: "u-url hover:underline",
42
+ children: /*#__PURE__*/ _jsx("time", {
43
+ class: "dt-published",
44
+ datetime: time.toISOString(post.publishedAt),
45
+ children: time.formatDate(post.publishedAt)
46
+ })
47
+ })
48
+ })
49
+ ]
50
+ });
51
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Thread Preview Component
3
+ *
4
+ * Inline thread preview: root card + compact replies + "show more" link.
5
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
+ import { useLingui as $_useLingui } from "@jant/core/i18n";
7
+ import { TimelineItem } from "./TimelineItem.js";
8
+ import * as sqid from "../../../lib/sqid.js";
9
+ export const ThreadPreview = ({ rootPost, previewReplies, totalReplyCount })=>{
10
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
11
+ const permalink = `/p/${sqid.encode(rootPost.id)}`;
12
+ const remainingCount = totalReplyCount - previewReplies.length;
13
+ return /*#__PURE__*/ _jsxs("div", {
14
+ class: "timeline-thread",
15
+ children: [
16
+ /*#__PURE__*/ _jsx(TimelineItem, {
17
+ item: {
18
+ post: rootPost
19
+ }
20
+ }),
21
+ previewReplies.length > 0 && /*#__PURE__*/ _jsxs("div", {
22
+ class: "timeline-thread-replies",
23
+ children: [
24
+ previewReplies.map((reply)=>/*#__PURE__*/ _jsx("div", {
25
+ class: "timeline-thread-reply",
26
+ children: /*#__PURE__*/ _jsx(TimelineItem, {
27
+ item: {
28
+ post: reply
29
+ },
30
+ compact: true
31
+ })
32
+ }, reply.id)),
33
+ remainingCount > 0 && /*#__PURE__*/ _jsx("div", {
34
+ class: "timeline-thread-reply",
35
+ children: /*#__PURE__*/ _jsx("a", {
36
+ href: permalink,
37
+ class: "text-sm text-muted-foreground hover:text-foreground hover:underline",
38
+ children: $__i18n._({
39
+ id: "smzF8S",
40
+ message: "Show {remainingCount} more {0}",
41
+ values: {
42
+ remainingCount: remainingCount,
43
+ 0: remainingCount === 1 ? "reply" : "replies"
44
+ }
45
+ })
46
+ })
47
+ })
48
+ ]
49
+ })
50
+ ]
51
+ });
52
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Timeline Feed Component
3
+ *
4
+ * Main feed wrapper with load-more button.
5
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
6
+ import { useLingui as $_useLingui } from "@jant/core/i18n";
7
+ import { TimelineItem } from "./TimelineItem.js";
8
+ import { ThreadPreview } from "./ThreadPreview.js";
9
+ export const TimelineFeed = ({ items, hasMore, nextCursor })=>{
10
+ const { i18n: $__i18n, _: $__ } = $_useLingui();
11
+ return /*#__PURE__*/ _jsxs("div", {
12
+ children: [
13
+ /*#__PURE__*/ _jsx("div", {
14
+ id: "timeline-feed",
15
+ class: "flex flex-col gap-4",
16
+ children: items.map((item)=>{
17
+ if (item.threadPreview) {
18
+ return /*#__PURE__*/ _jsx(ThreadPreview, {
19
+ rootPost: item.post,
20
+ previewReplies: item.threadPreview.replies,
21
+ totalReplyCount: item.threadPreview.totalReplyCount
22
+ }, item.post.id);
23
+ }
24
+ return /*#__PURE__*/ _jsx(TimelineItem, {
25
+ item: item
26
+ }, item.post.id);
27
+ })
28
+ }),
29
+ hasMore && nextCursor && /*#__PURE__*/ _jsx("div", {
30
+ id: "load-more-container",
31
+ class: "mt-6 text-center",
32
+ children: /*#__PURE__*/ _jsx("button", {
33
+ class: "btn btn-outline",
34
+ "data-on:click": `@get('/api/timeline?cursor=${nextCursor}')`,
35
+ children: $__i18n._({
36
+ id: "yQ2kGp",
37
+ message: "Load more"
38
+ })
39
+ })
40
+ })
41
+ ]
42
+ });
43
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Timeline Item Component
3
+ *
4
+ * Dispatches to the correct card component based on post type.
5
+ */ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
6
+ import { NoteCard } from "./NoteCard.js";
7
+ import { ArticleCard } from "./ArticleCard.js";
8
+ import { LinkCard } from "./LinkCard.js";
9
+ import { QuoteCard } from "./QuoteCard.js";
10
+ import { ImageCard } from "./ImageCard.js";
11
+ const CARD_MAP = {
12
+ note: NoteCard,
13
+ article: ArticleCard,
14
+ link: LinkCard,
15
+ quote: QuoteCard,
16
+ image: ImageCard,
17
+ page: NoteCard
18
+ };
19
+ export const TimelineItem = ({ item, compact, cardOverride })=>{
20
+ const Card = cardOverride ?? CARD_MAP[item.post.type];
21
+ return /*#__PURE__*/ _jsx(Card, {
22
+ post: item.post,
23
+ compact: compact
24
+ });
25
+ };
@@ -0,0 +1,8 @@
1
+ export { NoteCard } from "./NoteCard.js";
2
+ export { ArticleCard } from "./ArticleCard.js";
3
+ export { LinkCard } from "./LinkCard.js";
4
+ export { QuoteCard } from "./QuoteCard.js";
5
+ export { ImageCard } from "./ImageCard.js";
6
+ export { ThreadPreview } from "./ThreadPreview.js";
7
+ export { TimelineItem } from "./TimelineItem.js";
8
+ export { TimelineFeed } from "./TimelineFeed.js";
@@ -107,6 +107,14 @@ function DashLayoutContent({ siteName, currentPath, children }) {
107
107
  message: "Redirects"
108
108
  })
109
109
  }),
110
+ /*#__PURE__*/ _jsx("a", {
111
+ href: "/dash/navigation",
112
+ class: navClass("/dash/navigation", /^\/dash\/navigation/),
113
+ children: $__i18n._({
114
+ id: "UxKoFf",
115
+ message: "Navigation"
116
+ })
117
+ }),
110
118
  /*#__PURE__*/ _jsx("a", {
111
119
  href: "/dash/settings",
112
120
  class: navClass("/dash/settings", /^\/dash\/settings/),