@jant/core 0.3.36 → 0.3.37

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 (271) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  6. package/dist/client/client.css +1 -1
  7. package/dist/client/client.js +4012 -3276
  8. package/dist/index.js +10285 -5809
  9. package/package.json +11 -3
  10. package/src/__tests__/helpers/app.ts +9 -9
  11. package/src/__tests__/helpers/db.ts +91 -93
  12. package/src/app.tsx +157 -27
  13. package/src/auth.ts +20 -2
  14. package/src/client/archive-nav.js +187 -0
  15. package/src/client/audio-player.ts +478 -0
  16. package/src/client/audio-processor.ts +84 -0
  17. package/src/client/avatar-upload.ts +3 -2
  18. package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
  19. package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
  20. package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
  21. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  22. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  23. package/src/client/components/collection-sidebar-types.ts +7 -9
  24. package/src/client/components/compose-types.ts +101 -4
  25. package/src/client/components/jant-collection-form.ts +43 -7
  26. package/src/client/components/jant-collection-sidebar.ts +88 -84
  27. package/src/client/components/jant-compose-dialog.ts +1655 -219
  28. package/src/client/components/jant-compose-editor.ts +732 -168
  29. package/src/client/components/jant-compose-fullscreen.ts +23 -78
  30. package/src/client/components/jant-media-lightbox.ts +2 -0
  31. package/src/client/components/jant-nav-manager.ts +24 -284
  32. package/src/client/components/jant-post-form.ts +89 -9
  33. package/src/client/components/jant-post-menu.ts +1019 -0
  34. package/src/client/components/jant-settings-avatar.ts +3 -3
  35. package/src/client/components/jant-settings-general.ts +38 -4
  36. package/src/client/components/jant-text-preview.ts +232 -0
  37. package/src/client/components/nav-manager-types.ts +4 -19
  38. package/src/client/components/post-form-template.ts +107 -12
  39. package/src/client/components/post-form-types.ts +11 -4
  40. package/src/client/compose-bridge.ts +410 -109
  41. package/src/client/image-processor.ts +26 -8
  42. package/src/client/media-metadata.ts +247 -0
  43. package/src/client/multipart-upload.ts +160 -0
  44. package/src/client/post-form-bridge.ts +52 -1
  45. package/src/client/settings-bridge.ts +0 -12
  46. package/src/client/thread-context.ts +140 -0
  47. package/src/client/tiptap/create-editor.ts +46 -0
  48. package/src/client/tiptap/extensions.ts +5 -0
  49. package/src/client/tiptap/image-node.ts +2 -8
  50. package/src/client/tiptap/paste-image.ts +2 -13
  51. package/src/client/tiptap/slash-commands.ts +173 -63
  52. package/src/client/toast.ts +101 -3
  53. package/src/client/types/sortablejs.d.ts +15 -0
  54. package/src/client/upload-with-metadata.ts +54 -0
  55. package/src/client/video-processor.ts +207 -0
  56. package/src/client.ts +5 -2
  57. package/src/db/__tests__/migrations.test.ts +118 -0
  58. package/src/db/index.ts +52 -0
  59. package/src/db/migrations/0000_baseline.sql +269 -0
  60. package/src/db/migrations/0001_fts_setup.sql +31 -0
  61. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  62. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  63. package/src/db/migrations/meta/_journal.json +4 -39
  64. package/src/db/schema.ts +409 -145
  65. package/src/i18n/__tests__/detect.test.ts +115 -0
  66. package/src/i18n/context.tsx +2 -2
  67. package/src/i18n/detect.ts +85 -1
  68. package/src/i18n/i18n.ts +1 -1
  69. package/src/i18n/index.ts +2 -1
  70. package/src/i18n/locales/en.po +487 -1217
  71. package/src/i18n/locales/en.ts +1 -1
  72. package/src/i18n/locales/zh-Hans.po +613 -996
  73. package/src/i18n/locales/zh-Hans.ts +1 -1
  74. package/src/i18n/locales/zh-Hant.po +624 -1007
  75. package/src/i18n/locales/zh-Hant.ts +1 -1
  76. package/src/i18n/middleware.ts +6 -0
  77. package/src/index.ts +5 -7
  78. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  79. package/src/lib/__tests__/constants.test.ts +0 -1
  80. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  81. package/src/lib/__tests__/nanoid.test.ts +26 -0
  82. package/src/lib/__tests__/schemas.test.ts +181 -63
  83. package/src/lib/__tests__/slug.test.ts +126 -0
  84. package/src/lib/__tests__/sse.test.ts +6 -6
  85. package/src/lib/__tests__/summary.test.ts +264 -0
  86. package/src/lib/__tests__/theme.test.ts +1 -1
  87. package/src/lib/__tests__/timeline.test.ts +33 -30
  88. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  89. package/src/lib/__tests__/view.test.ts +141 -66
  90. package/src/lib/blurhash-placeholder.ts +102 -0
  91. package/src/lib/constants.ts +3 -1
  92. package/src/lib/emoji-catalog.ts +885 -68
  93. package/src/lib/errors.ts +11 -8
  94. package/src/lib/feed.ts +78 -32
  95. package/src/lib/html.ts +2 -1
  96. package/src/lib/icon-catalog.ts +5033 -1
  97. package/src/lib/icons.ts +3 -2
  98. package/src/lib/index.ts +0 -1
  99. package/src/lib/markdown-to-tiptap.ts +286 -0
  100. package/src/lib/media-helpers.ts +12 -3
  101. package/src/lib/nanoid.ts +29 -0
  102. package/src/lib/navigation.ts +1 -1
  103. package/src/lib/render.tsx +20 -2
  104. package/src/lib/resolve-config.ts +6 -2
  105. package/src/lib/schemas.ts +224 -55
  106. package/src/lib/search-snippet.ts +34 -0
  107. package/src/lib/slug.ts +96 -0
  108. package/src/lib/sse.ts +6 -6
  109. package/src/lib/storage.ts +115 -7
  110. package/src/lib/summary.ts +66 -0
  111. package/src/lib/theme.ts +11 -8
  112. package/src/lib/timeline.ts +74 -34
  113. package/src/lib/tiptap-render.ts +5 -10
  114. package/src/lib/tiptap-to-markdown.ts +305 -0
  115. package/src/lib/upload.ts +190 -29
  116. package/src/lib/url.ts +31 -0
  117. package/src/lib/view.ts +204 -37
  118. package/src/middleware/__tests__/auth.test.ts +191 -11
  119. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  120. package/src/middleware/auth.ts +63 -9
  121. package/src/middleware/onboarding.ts +1 -1
  122. package/src/middleware/secure-headers.ts +40 -0
  123. package/src/preset.css +45 -2
  124. package/src/routes/__tests__/compose.test.ts +17 -24
  125. package/src/routes/api/__tests__/collections.test.ts +109 -61
  126. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  127. package/src/routes/api/__tests__/posts.test.ts +132 -68
  128. package/src/routes/api/__tests__/search.test.ts +15 -2
  129. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  130. package/src/routes/api/collections.ts +51 -42
  131. package/src/routes/api/custom-urls.ts +80 -0
  132. package/src/routes/api/export.ts +31 -0
  133. package/src/routes/api/nav-items.ts +23 -19
  134. package/src/routes/api/posts.ts +43 -39
  135. package/src/routes/api/search.ts +3 -4
  136. package/src/routes/api/upload-multipart.ts +245 -0
  137. package/src/routes/api/upload.ts +85 -19
  138. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  139. package/src/routes/auth/setup.tsx +26 -33
  140. package/src/routes/auth/signin.tsx +3 -7
  141. package/src/routes/compose.tsx +10 -55
  142. package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
  143. package/src/routes/dash/custom-urls.tsx +414 -0
  144. package/src/routes/dash/settings.tsx +304 -232
  145. package/src/routes/feed/__tests__/rss.test.ts +27 -28
  146. package/src/routes/feed/rss.ts +6 -4
  147. package/src/routes/feed/sitemap.ts +2 -12
  148. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  149. package/src/routes/pages/__tests__/featured.test.ts +41 -22
  150. package/src/routes/pages/archive.tsx +175 -39
  151. package/src/routes/pages/collection.tsx +22 -10
  152. package/src/routes/pages/collections.tsx +3 -3
  153. package/src/routes/pages/featured.tsx +28 -4
  154. package/src/routes/pages/home.tsx +16 -15
  155. package/src/routes/pages/latest.tsx +1 -11
  156. package/src/routes/pages/new.tsx +39 -0
  157. package/src/routes/pages/page.tsx +95 -49
  158. package/src/routes/pages/search.tsx +1 -1
  159. package/src/services/__tests__/api-token.test.ts +135 -0
  160. package/src/services/__tests__/collection.test.ts +275 -227
  161. package/src/services/__tests__/custom-url.test.ts +213 -0
  162. package/src/services/__tests__/media.test.ts +162 -22
  163. package/src/services/__tests__/navigation.test.ts +109 -68
  164. package/src/services/__tests__/post-timeline.test.ts +205 -32
  165. package/src/services/__tests__/post.test.ts +713 -234
  166. package/src/services/__tests__/search.test.ts +67 -10
  167. package/src/services/api-token.ts +166 -0
  168. package/src/services/auth.ts +17 -2
  169. package/src/services/collection.ts +397 -131
  170. package/src/services/custom-url.ts +188 -0
  171. package/src/services/export.ts +802 -0
  172. package/src/services/index.ts +26 -19
  173. package/src/services/media.ts +100 -22
  174. package/src/services/navigation.ts +158 -47
  175. package/src/services/path.ts +339 -0
  176. package/src/services/post.ts +687 -154
  177. package/src/services/search.ts +160 -75
  178. package/src/styles/components.css +58 -7
  179. package/src/styles/tokens.css +84 -6
  180. package/src/styles/ui.css +2964 -457
  181. package/src/types/bindings.ts +4 -1
  182. package/src/types/config.ts +12 -4
  183. package/src/types/constants.ts +15 -3
  184. package/src/types/entities.ts +74 -35
  185. package/src/types/operations.ts +11 -24
  186. package/src/types/props.ts +51 -16
  187. package/src/types/views.ts +45 -22
  188. package/src/ui/color-themes.ts +133 -23
  189. package/src/ui/compose/ComposeDialog.tsx +239 -17
  190. package/src/ui/compose/ComposePrompt.tsx +1 -1
  191. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  192. package/src/ui/dash/ListItemRow.tsx +1 -1
  193. package/src/ui/dash/StatusBadge.tsx +3 -1
  194. package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
  195. package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
  196. package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
  197. package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
  198. package/src/ui/dash/index.ts +0 -3
  199. package/src/ui/dash/settings/AccountContent.tsx +3 -57
  200. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  201. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  202. package/src/ui/dash/settings/AvatarContent.tsx +8 -0
  203. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  204. package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
  205. package/src/ui/feed/LinkCard.tsx +89 -40
  206. package/src/ui/feed/NoteCard.tsx +39 -25
  207. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  208. package/src/ui/feed/QuoteCard.tsx +38 -23
  209. package/src/ui/feed/ThreadPreview.tsx +90 -26
  210. package/src/ui/feed/TimelineFeed.tsx +3 -2
  211. package/src/ui/feed/TimelineItem.tsx +15 -6
  212. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  213. package/src/ui/feed/thread-preview-state.ts +61 -0
  214. package/src/ui/font-themes.ts +2 -2
  215. package/src/ui/layouts/BaseLayout.tsx +2 -2
  216. package/src/ui/layouts/SiteLayout.tsx +105 -92
  217. package/src/ui/pages/ArchivePage.tsx +923 -98
  218. package/src/ui/pages/ComposePage.tsx +54 -0
  219. package/src/ui/pages/PostPage.tsx +30 -45
  220. package/src/ui/pages/SearchPage.tsx +181 -37
  221. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  222. package/src/ui/shared/CollectionsSidebar.tsx +47 -37
  223. package/src/ui/shared/MediaGallery.tsx +445 -149
  224. package/src/ui/shared/PostFooter.tsx +204 -0
  225. package/src/ui/shared/StarRating.tsx +27 -0
  226. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  227. package/src/ui/shared/index.ts +0 -1
  228. package/dist/client/assets/url-8Dj-5CLW.js +0 -1
  229. package/src/client/media-upload.ts +0 -161
  230. package/src/client/page-slug-bridge.ts +0 -42
  231. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  232. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  233. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  234. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  235. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  236. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  237. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  238. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  239. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  240. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  241. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  242. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  243. package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
  244. package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
  245. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  246. package/src/lib/__tests__/sqid.test.ts +0 -65
  247. package/src/lib/sqid.ts +0 -79
  248. package/src/routes/api/__tests__/pages.test.ts +0 -218
  249. package/src/routes/api/pages.ts +0 -73
  250. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  251. package/src/routes/dash/index.tsx +0 -109
  252. package/src/routes/dash/media.tsx +0 -135
  253. package/src/routes/dash/pages.tsx +0 -245
  254. package/src/routes/dash/posts.tsx +0 -338
  255. package/src/routes/dash/redirects.tsx +0 -263
  256. package/src/routes/pages/post.tsx +0 -59
  257. package/src/services/__tests__/page.test.ts +0 -298
  258. package/src/services/__tests__/path-registry.test.ts +0 -165
  259. package/src/services/__tests__/redirect.test.ts +0 -159
  260. package/src/services/page.ts +0 -216
  261. package/src/services/path-registry.ts +0 -160
  262. package/src/services/redirect.ts +0 -97
  263. package/src/ui/dash/PageForm.tsx +0 -187
  264. package/src/ui/dash/PostList.tsx +0 -95
  265. package/src/ui/dash/media/MediaListContent.tsx +0 -206
  266. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  267. package/src/ui/dash/pages/PagesContent.tsx +0 -75
  268. package/src/ui/dash/posts/PostForm.tsx +0 -260
  269. package/src/ui/layouts/DashLayout.tsx +0 -247
  270. package/src/ui/pages/SinglePage.tsx +0 -23
  271. package/src/ui/shared/ThreadView.tsx +0 -136
@@ -1,338 +0,0 @@
1
- /**
2
- * Dashboard Posts Routes
3
- */
4
-
5
- import { Hono } from "hono";
6
- import { useLingui } from "@lingui/react/macro";
7
- import type {
8
- Bindings,
9
- Post,
10
- PostView,
11
- Media,
12
- Collection,
13
- } from "../../types.js";
14
- import type { AppVariables } from "../../types/app-context.js";
15
- import { DashLayout } from "../../ui/layouts/DashLayout.js";
16
- import {
17
- PostForm,
18
- PostList,
19
- CrudPageHeader,
20
- ActionButtons,
21
- } from "../../ui/dash/index.js";
22
- import * as sqid from "../../lib/sqid.js";
23
- import { dsRedirect } from "../../lib/sse.js";
24
- import {
25
- CreatePostSchema,
26
- UpdatePostSchema,
27
- parseValidated,
28
- } from "../../lib/schemas.js";
29
- import {
30
- toPostViewsFromPosts,
31
- toPostViewFromPost,
32
- createMediaContext,
33
- } from "../../lib/view.js";
34
-
35
- type Env = { Bindings: Bindings; Variables: AppVariables };
36
-
37
- export const postsRoutes = new Hono<Env>();
38
-
39
- function PostsListContent({ posts }: { posts: PostView[] }) {
40
- const { t } = useLingui();
41
- return (
42
- <>
43
- <CrudPageHeader
44
- title={t({ message: "Posts", comment: "@context: Dashboard heading" })}
45
- ctaLabel={t({
46
- message: "New Post",
47
- comment: "@context: Button to create new post",
48
- })}
49
- ctaHref="/dash/posts/new"
50
- />
51
- <PostList posts={posts} />
52
- </>
53
- );
54
- }
55
-
56
- function NewPostContent({ collections }: { collections: Collection[] }) {
57
- const { t } = useLingui();
58
- return (
59
- <>
60
- <h1 class="text-2xl font-semibold mb-6">
61
- {t({ message: "New Post", comment: "@context: Page heading" })}
62
- </h1>
63
- <PostForm action="/dash/posts" collections={collections} />
64
- </>
65
- );
66
- }
67
-
68
- // List posts
69
- postsRoutes.get("/", async (c) => {
70
- const posts = await c.var.services.posts.list({
71
- excludeReplies: true,
72
- });
73
- const siteName = c.var.appConfig.siteName;
74
- const postViews = toPostViewsFromPosts(
75
- posts,
76
- createMediaContext(c.var.appConfig),
77
- );
78
-
79
- return c.html(
80
- <DashLayout
81
- c={c}
82
- title="Posts"
83
- siteName={siteName}
84
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
85
- currentPath="/dash/posts"
86
- >
87
- <PostsListContent posts={postViews} />
88
- </DashLayout>,
89
- );
90
- });
91
-
92
- // New post form
93
- postsRoutes.get("/new", async (c) => {
94
- const siteName = c.var.appConfig.siteName;
95
- const collections = await c.var.services.collections.list();
96
-
97
- return c.html(
98
- <DashLayout
99
- c={c}
100
- title="New Post"
101
- siteName={siteName}
102
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
103
- currentPath="/dash/posts"
104
- >
105
- <NewPostContent collections={collections} />
106
- </DashLayout>,
107
- );
108
- });
109
-
110
- // Create post
111
- postsRoutes.post("/", async (c) => {
112
- const wantsJson = c.req.header("Accept")?.includes("application/json");
113
- const body = parseValidated(CreatePostSchema, await c.req.json());
114
-
115
- // Validate media IDs before creating post
116
- if (body.mediaIds && body.mediaIds.length > 0) {
117
- await c.var.services.media.validateIds(body.mediaIds);
118
- }
119
-
120
- const post = await c.var.services.posts.create({
121
- format: body.format,
122
- title: body.title || undefined,
123
- body: body.body,
124
- status: body.status,
125
- visibility: body.visibility,
126
- pinned: body.pinned,
127
- url: body.url || undefined,
128
- quoteText: body.quoteText || undefined,
129
- rating: body.rating || undefined,
130
- collectionIds: body.collectionIds?.length ? body.collectionIds : undefined,
131
- });
132
-
133
- // Attach media if provided
134
- if (body.mediaIds && body.mediaIds.length > 0) {
135
- await c.var.services.media.attachToPost(post.id, body.mediaIds);
136
- }
137
-
138
- const redirectUrl = `/dash/posts/${sqid.encode(post.id)}`;
139
- if (wantsJson) {
140
- return c.json({ status: "redirect" as const, url: redirectUrl });
141
- }
142
-
143
- return dsRedirect(redirectUrl);
144
- });
145
-
146
- function ViewPostContent({ post }: { post: PostView }) {
147
- const { t } = useLingui();
148
- const defaultTitle = t({
149
- message: "Post",
150
- comment: "@context: Default post title",
151
- });
152
-
153
- return (
154
- <>
155
- <div class="flex items-center justify-between mb-6">
156
- <h1 class="text-2xl font-semibold">{post.title || defaultTitle}</h1>
157
- <ActionButtons
158
- editHref={`/dash/posts/${sqid.encode(post.id)}/edit`}
159
- editLabel={t({
160
- message: "Edit",
161
- comment: "@context: Button to edit post",
162
- })}
163
- viewHref={post.permalink}
164
- viewLabel={t({
165
- message: "View",
166
- comment: "@context: Button to view post",
167
- })}
168
- />
169
- </div>
170
-
171
- <div class="card">
172
- <section>
173
- <div
174
- class="prose"
175
- dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
176
- />
177
- </section>
178
- </div>
179
- </>
180
- );
181
- }
182
-
183
- function EditPostContent({
184
- post,
185
- mediaAttachments,
186
- r2PublicUrl,
187
- imageTransformUrl,
188
- s3PublicUrl,
189
- collections,
190
- postCollectionIds,
191
- }: {
192
- post: Post;
193
- mediaAttachments: Media[];
194
- r2PublicUrl?: string;
195
- imageTransformUrl?: string;
196
- s3PublicUrl?: string;
197
- collections: Collection[];
198
- postCollectionIds: number[];
199
- }) {
200
- const { t } = useLingui();
201
- return (
202
- <>
203
- <h1 class="text-2xl font-semibold mb-6">
204
- {t({ message: "Edit Post", comment: "@context: Page heading" })}
205
- </h1>
206
- <PostForm
207
- post={post}
208
- action={`/dash/posts/${sqid.encode(post.id)}`}
209
- mediaAttachments={mediaAttachments}
210
- r2PublicUrl={r2PublicUrl}
211
- imageTransformUrl={imageTransformUrl}
212
- s3PublicUrl={s3PublicUrl}
213
- collections={collections}
214
- postCollectionIds={postCollectionIds}
215
- cancelHref={`/dash/posts/${sqid.encode(post.id)}`}
216
- />
217
- </>
218
- );
219
- }
220
-
221
- // View single post
222
- postsRoutes.get("/:id", async (c) => {
223
- const id = sqid.decode(c.req.param("id"));
224
- if (!id) return c.notFound();
225
-
226
- const post = await c.var.services.posts.getById(id);
227
- if (!post) return c.notFound();
228
-
229
- const siteName = c.var.appConfig.siteName;
230
- const pageTitle = post.title || "Post";
231
- const postView = toPostViewFromPost(
232
- post,
233
- createMediaContext(c.var.appConfig),
234
- );
235
-
236
- return c.html(
237
- <DashLayout
238
- c={c}
239
- title={pageTitle}
240
- siteName={siteName}
241
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
242
- currentPath="/dash/posts"
243
- >
244
- <ViewPostContent post={postView} />
245
- </DashLayout>,
246
- );
247
- });
248
-
249
- // Edit post form
250
- postsRoutes.get("/:id/edit", async (c) => {
251
- const id = sqid.decode(c.req.param("id"));
252
- if (!id) return c.notFound();
253
-
254
- const post = await c.var.services.posts.getById(id);
255
- if (!post) return c.notFound();
256
-
257
- const siteName = c.var.appConfig.siteName;
258
- const mediaAttachments = await c.var.services.media.getByPostId(post.id);
259
- const { r2PublicUrl, imageTransformUrl, s3PublicUrl } = c.var.appConfig;
260
- const [collections, postCollections] = await Promise.all([
261
- c.var.services.collections.list(),
262
- c.var.services.collections.getCollectionsByPostId(post.id),
263
- ]);
264
- const postCollectionIds = postCollections.map((c) => c.id);
265
-
266
- return c.html(
267
- <DashLayout
268
- c={c}
269
- title={`Edit: ${post.title || "Post"}`}
270
- siteName={siteName}
271
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
272
- currentPath="/dash/posts"
273
- >
274
- <EditPostContent
275
- post={post}
276
- mediaAttachments={mediaAttachments}
277
- r2PublicUrl={r2PublicUrl}
278
- imageTransformUrl={imageTransformUrl}
279
- s3PublicUrl={s3PublicUrl}
280
- collections={collections}
281
- postCollectionIds={postCollectionIds}
282
- />
283
- </DashLayout>,
284
- );
285
- });
286
-
287
- // Update post
288
- postsRoutes.post("/:id", async (c) => {
289
- const id = sqid.decode(c.req.param("id"));
290
- if (!id) return c.notFound();
291
-
292
- const wantsJson = c.req.header("Accept")?.includes("application/json");
293
-
294
- const body = parseValidated(UpdatePostSchema, await c.req.json());
295
-
296
- // Validate media IDs if provided
297
- if (body.mediaIds !== undefined) {
298
- await c.var.services.media.validateIds(body.mediaIds);
299
- }
300
-
301
- await c.var.services.posts.update(id, {
302
- format: body.format,
303
- title: body.title || null,
304
- body: body.body || null,
305
- status: body.status,
306
- visibility: body.visibility,
307
- pinned: body.pinned,
308
- url: body.url || null,
309
- quoteText: body.quoteText || null,
310
- rating: body.rating || null,
311
- collectionIds: body.collectionIds ?? [],
312
- });
313
-
314
- // Update media attachments if provided
315
- if (body.mediaIds !== undefined) {
316
- await c.var.services.media.attachToPost(id, body.mediaIds);
317
- }
318
-
319
- const redirectUrl = `/dash/posts/${sqid.encode(id)}`;
320
- if (wantsJson) {
321
- return c.json({ status: "redirect" as const, url: redirectUrl });
322
- }
323
-
324
- return dsRedirect(redirectUrl);
325
- });
326
-
327
- // Delete post
328
- postsRoutes.post("/:id/delete", async (c) => {
329
- const id = sqid.decode(c.req.param("id"));
330
- if (!id) return c.notFound();
331
-
332
- await c.var.services.posts.delete(id, {
333
- media: c.var.services.media,
334
- storage: c.var.storage,
335
- });
336
-
337
- return dsRedirect("/dash/posts");
338
- });
@@ -1,263 +0,0 @@
1
- /**
2
- * Dashboard Redirects Routes
3
- *
4
- * Mounted under /dash/settings/redirects
5
- */
6
-
7
- import { Hono } from "hono";
8
- import { z } from "zod";
9
- import { useLingui } from "@lingui/react/macro";
10
- import type { Bindings, Redirect } from "../../types.js";
11
- import type { AppVariables } from "../../types/app-context.js";
12
- import { DashLayout } from "../../ui/layouts/DashLayout.js";
13
- import { EmptyState, ListItemRow, ActionButtons } from "../../ui/dash/index.js";
14
- import { dsRedirect } from "../../lib/sse.js";
15
- import { RedirectTypeSchema, parseValidated } from "../../lib/schemas.js";
16
-
17
- type Env = { Bindings: Bindings; Variables: AppVariables };
18
-
19
- const CreateRedirectBody = z.object({
20
- fromPath: z.string().min(1),
21
- toPath: z.string().min(1),
22
- type: RedirectTypeSchema,
23
- });
24
-
25
- export const redirectsRoutes = new Hono<Env>();
26
-
27
- function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
28
- const { t } = useLingui();
29
-
30
- return (
31
- <>
32
- <div class="flex items-center justify-between mb-6">
33
- <h2 class="text-lg font-medium">
34
- {t({
35
- message: "Redirects",
36
- comment: "@context: Settings section heading",
37
- })}
38
- </h2>
39
- <a href="/dash/settings/redirects/new" class="btn">
40
- {t({
41
- message: "New Redirect",
42
- comment: "@context: Button to create new redirect",
43
- })}
44
- </a>
45
- </div>
46
-
47
- {redirects.length === 0 ? (
48
- <EmptyState
49
- message={t({
50
- message:
51
- "No redirects yet. Create one to forward traffic from old URLs.",
52
- comment: "@context: Empty state message",
53
- })}
54
- ctaText={t({
55
- message: "New Redirect",
56
- comment: "@context: Button to create new redirect",
57
- })}
58
- ctaHref="/dash/settings/redirects/new"
59
- />
60
- ) : (
61
- <div class="flex flex-col divide-y">
62
- {redirects.map((r) => (
63
- <ListItemRow
64
- key={r.id}
65
- actions={
66
- <ActionButtons
67
- deleteAction={`/dash/settings/redirects/${r.id}/delete`}
68
- deleteLabel={t({
69
- message: "Delete",
70
- comment: "@context: Button to delete redirect",
71
- })}
72
- />
73
- }
74
- >
75
- <div class="flex items-center gap-2">
76
- <code class="text-sm bg-muted px-1 rounded">{r.fromPath}</code>
77
- <span class="text-muted-foreground">&rarr;</span>
78
- <code class="text-sm bg-muted px-1 rounded">{r.toPath}</code>
79
- <span class="badge-outline">{r.type}</span>
80
- </div>
81
- </ListItemRow>
82
- ))}
83
- </div>
84
- )}
85
- </>
86
- );
87
- }
88
-
89
- function NewRedirectContent() {
90
- const { t } = useLingui();
91
-
92
- return (
93
- <>
94
- <h2 class="text-lg font-medium mb-6">
95
- {t({ message: "New Redirect", comment: "@context: Page heading" })}
96
- </h2>
97
-
98
- <form
99
- data-signals="{fromPath: '', toPath: '', type: '301'}"
100
- data-on:submit__prevent="@post('/dash/settings/redirects')"
101
- data-indicator="_loading"
102
- class="flex flex-col gap-4 max-w-lg"
103
- >
104
- <div class="field">
105
- <label class="label">
106
- {t({
107
- message: "From Path",
108
- comment: "@context: Redirect form field",
109
- })}
110
- </label>
111
- <input
112
- type="text"
113
- data-bind="fromPath"
114
- class="input"
115
- placeholder="/old-path"
116
- required
117
- />
118
- <p class="text-xs text-muted-foreground mt-1">
119
- {t({
120
- message: "The path to redirect from",
121
- comment: "@context: Redirect from path help text",
122
- })}
123
- </p>
124
- </div>
125
-
126
- <div class="field">
127
- <label class="label">
128
- {t({
129
- message: "To Path",
130
- comment: "@context: Redirect form field",
131
- })}
132
- </label>
133
- <input
134
- type="text"
135
- data-bind="toPath"
136
- class="input"
137
- placeholder="/new-path or https://..."
138
- required
139
- />
140
- <p class="text-xs text-muted-foreground mt-1">
141
- {t({
142
- message: "The destination path or URL",
143
- comment: "@context: Redirect to path help text",
144
- })}
145
- </p>
146
- </div>
147
-
148
- <div class="field">
149
- <label class="label">
150
- {t({ message: "Type", comment: "@context: Redirect form field" })}
151
- </label>
152
- <select data-bind="type" class="select">
153
- <option value="301">
154
- {t({
155
- message: "301 (Permanent)",
156
- comment: "@context: Redirect type option",
157
- })}
158
- </option>
159
- <option value="302">
160
- {t({
161
- message: "302 (Temporary)",
162
- comment: "@context: Redirect type option",
163
- })}
164
- </option>
165
- </select>
166
- </div>
167
-
168
- <div class="flex gap-2">
169
- <button type="submit" class="btn" data-attr:disabled="$_loading">
170
- <svg
171
- data-show="$_loading"
172
- style="display:none"
173
- class="animate-spin size-4"
174
- xmlns="http://www.w3.org/2000/svg"
175
- viewBox="0 0 24 24"
176
- fill="none"
177
- stroke="currentColor"
178
- stroke-width="2"
179
- stroke-linecap="round"
180
- stroke-linejoin="round"
181
- role="status"
182
- >
183
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
184
- </svg>
185
- {t({
186
- message: "Create Redirect",
187
- comment: "@context: Button to save new redirect",
188
- })}
189
- </button>
190
- <a href="/dash/settings/redirects" class="btn-outline">
191
- {t({
192
- message: "Cancel",
193
- comment: "@context: Button to cancel form",
194
- })}
195
- </a>
196
- </div>
197
- </form>
198
- </>
199
- );
200
- }
201
-
202
- const BREADCRUMB = {
203
- parent: "Settings",
204
- parentHref: "/dash/settings",
205
- current: "Redirects",
206
- };
207
-
208
- // List redirects
209
- redirectsRoutes.get("/", async (c) => {
210
- const siteName = c.var.appConfig.siteName;
211
- const redirects = await c.var.services.redirects.list();
212
-
213
- return c.html(
214
- <DashLayout
215
- c={c}
216
- title="Redirects"
217
- siteName={siteName}
218
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
219
- currentPath="/dash/settings"
220
- breadcrumb={BREADCRUMB}
221
- >
222
- <RedirectsListContent redirects={redirects} />
223
- </DashLayout>,
224
- );
225
- });
226
-
227
- // New redirect form
228
- redirectsRoutes.get("/new", async (c) => {
229
- const siteName = c.var.appConfig.siteName;
230
-
231
- return c.html(
232
- <DashLayout
233
- c={c}
234
- title="Redirects"
235
- siteName={siteName}
236
- siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
237
- currentPath="/dash/settings"
238
- breadcrumb={BREADCRUMB}
239
- >
240
- <NewRedirectContent />
241
- </DashLayout>,
242
- );
243
- });
244
-
245
- // Create redirect
246
- redirectsRoutes.post("/", async (c) => {
247
- const body = parseValidated(CreateRedirectBody, await c.req.json());
248
-
249
- const type = parseInt(body.type, 10) as 301 | 302;
250
- await c.var.services.redirects.create(body.fromPath, body.toPath, type);
251
-
252
- return dsRedirect("/dash/settings/redirects");
253
- });
254
-
255
- // Delete redirect
256
- redirectsRoutes.post("/:id/delete", async (c) => {
257
- const id = parseInt(c.req.param("id"), 10);
258
- if (!isNaN(id)) {
259
- await c.var.services.redirects.delete(id);
260
- }
261
-
262
- return dsRedirect("/dash/settings/redirects");
263
- });
@@ -1,59 +0,0 @@
1
- /**
2
- * Single Post Page Route
3
- */
4
-
5
- import { Hono } from "hono";
6
- import type { Bindings } from "../../types.js";
7
- import type { AppVariables } from "../../types/app-context.js";
8
- import { PostPage } from "../../ui/pages/PostPage.js";
9
- import * as sqid from "../../lib/sqid.js";
10
- import { getNavigationData } from "../../lib/navigation.js";
11
- import { renderPublicPage } from "../../lib/render.js";
12
- import { buildMediaMap } from "../../lib/media-helpers.js";
13
- import { createMediaContext, toPostView } from "../../lib/view.js";
14
-
15
- type Env = { Bindings: Bindings; Variables: AppVariables };
16
-
17
- export const postRoutes = new Hono<Env>();
18
-
19
- postRoutes.get("/:id", async (c) => {
20
- const paramId = c.req.param("id");
21
-
22
- // Decode sqid to numeric ID
23
- const id = sqid.decode(paramId);
24
- if (!id) return c.notFound();
25
-
26
- const post = await c.var.services.posts.getById(id);
27
- if (!post) return c.notFound();
28
-
29
- // Don't show drafts on public site
30
- if (post.status === "draft") {
31
- return c.notFound();
32
- }
33
-
34
- // Batch load media attachments
35
- const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
36
- const mediaCtx = createMediaContext(c.var.appConfig);
37
- const mediaMap = buildMediaMap(
38
- rawMediaMap,
39
- mediaCtx.r2PublicUrl,
40
- mediaCtx.imageTransformUrl,
41
- mediaCtx.s3PublicUrl,
42
- );
43
-
44
- // Transform to View Model
45
- const postView = toPostView(
46
- { ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
47
- mediaCtx,
48
- );
49
-
50
- const navData = await getNavigationData(c);
51
- const title = post.title || navData.siteName;
52
-
53
- return renderPublicPage(c, {
54
- title,
55
- description: post.body?.slice(0, 160),
56
- navData,
57
- content: <PostPage post={postView} />,
58
- });
59
- });