@jant/core 0.3.36 → 0.3.38

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,260 +0,0 @@
1
- /**
2
- * Post Form
3
- *
4
- * Server-rendered wrapper that feeds data/labels to `<jant-post-form>`.
5
- * Provides SSR fallback skeleton while Lit hydrates.
6
- */
7
-
8
- import { useLingui } from "@lingui/react/macro";
9
- import type { FC } from "hono/jsx";
10
- import type { Post, Media, Collection } from "../../../types.js";
11
- import {
12
- getMediaUrl,
13
- getImageUrl,
14
- getPublicUrlForProvider,
15
- } from "../../../lib/image.js";
16
- import { renderCollectionIcon } from "../../../lib/icons.js";
17
-
18
- export interface PostFormProps {
19
- post?: Post;
20
- action: string;
21
- mediaAttachments?: Media[];
22
- r2PublicUrl?: string;
23
- imageTransformUrl?: string;
24
- s3PublicUrl?: string;
25
- collections?: Collection[];
26
- postCollectionIds?: number[];
27
- cancelHref?: string;
28
- }
29
-
30
- export const PostForm: FC<PostFormProps> = ({
31
- post,
32
- action,
33
- mediaAttachments = [],
34
- r2PublicUrl,
35
- imageTransformUrl,
36
- s3PublicUrl,
37
- collections = [],
38
- postCollectionIds = [],
39
- cancelHref,
40
- }) => {
41
- const { t } = useLingui();
42
- const isEdit = Boolean(post);
43
-
44
- const labels = JSON.stringify({
45
- formatLabel: t({
46
- message: "Format",
47
- comment: "@context: Post form field - post format",
48
- }),
49
- noteOption: t({
50
- message: "Note",
51
- comment: "@context: Post format option",
52
- }),
53
- linkOption: t({
54
- message: "Link",
55
- comment: "@context: Post format option",
56
- }),
57
- quoteOption: t({
58
- message: "Quote",
59
- comment: "@context: Post format option",
60
- }),
61
- titleLabel: t({
62
- message: "Title (optional)",
63
- comment: "@context: Post form field",
64
- }),
65
- titlePlaceholder: t({
66
- message: "Post title...",
67
- comment: "@context: Post title placeholder",
68
- }),
69
- bodyLabel: t({
70
- message: "Content",
71
- comment: "@context: Post form field",
72
- }),
73
- bodyPlaceholder: t({
74
- message: "What's on your mind?",
75
- comment: "@context: Post content placeholder",
76
- }),
77
- urlLabel: t({
78
- message: "URL (optional)",
79
- comment: "@context: Post form field - source URL",
80
- }),
81
- urlPlaceholder: "https://...",
82
- quoteTextLabel: t({
83
- message: "Quote Text",
84
- comment: "@context: Post form field - quoted text",
85
- }),
86
- quoteTextPlaceholder: t({
87
- message: "The text being quoted...",
88
- comment: "@context: Quote text placeholder",
89
- }),
90
- mediaLabel: t({
91
- message: "Media",
92
- comment: "@context: Post form field - media attachments",
93
- }),
94
- mediaAddButton: t({
95
- message: "Add Media",
96
- comment: "@context: Button to open media picker",
97
- }),
98
- mediaRemoveButton: t({
99
- message: "Remove",
100
- comment: "@context: Remove media attachment button",
101
- }),
102
- mediaEmptyLabel: t({
103
- message: "No media attached.",
104
- comment: "@context: Post form media empty state",
105
- }),
106
- statusLabel: t({
107
- message: "Status",
108
- comment: "@context: Post form field",
109
- }),
110
- statusPublished: t({
111
- message: "Published",
112
- comment: "@context: Post status option",
113
- }),
114
- statusDraft: t({
115
- message: "Draft",
116
- comment: "@context: Post status option",
117
- }),
118
- visibilityLabel: t({
119
- message: "Visibility",
120
- comment: "@context: Post form field - post visibility",
121
- }),
122
- visibilityListed: t({
123
- message: "Listed",
124
- comment: "@context: Visibility option - appears everywhere",
125
- }),
126
- visibilityFeatured: t({
127
- message: "Featured",
128
- comment: "@context: Visibility option - highlighted on featured page",
129
- }),
130
- visibilityUnlisted: t({
131
- message: "Unlisted",
132
- comment: "@context: Visibility option - hidden from feeds",
133
- }),
134
- pinnedLabel: t({
135
- message: "Pinned",
136
- comment: "@context: Post form checkbox - pin to top",
137
- }),
138
- collectionsLabel: t({
139
- message: "Collections (optional)",
140
- comment: "@context: Post form field - assign to collections",
141
- }),
142
- submitLabel: isEdit
143
- ? t({
144
- message: "Update",
145
- comment: "@context: Button to update existing post",
146
- })
147
- : t({
148
- message: "Publish",
149
- comment: "@context: Button to publish new post",
150
- }),
151
- cancelLabel: t({
152
- message: "Cancel",
153
- comment: "@context: Button to cancel form",
154
- }),
155
- mediaDialogTitle: t({
156
- message: "Select Media",
157
- comment: "@context: Media picker dialog title",
158
- }),
159
- mediaDialogDone: t({
160
- message: "Done",
161
- comment: "@context: Close media picker button",
162
- }),
163
- mediaDialogLoading: t({
164
- message: "Loading...",
165
- comment: "@context: Loading state for media picker",
166
- }),
167
- submitSuccessMessage: isEdit
168
- ? t({
169
- message: "Post updated.",
170
- comment: "@context: Toast after editing post",
171
- })
172
- : t({
173
- message: "Post published.",
174
- comment: "@context: Toast after creating post",
175
- }),
176
- submitErrorMessage: t({
177
- message: "Couldn't save your post. Try again in a moment.",
178
- comment: "@context: Toast when post save fails",
179
- }),
180
- }).replace(/</g, "\\u003c");
181
-
182
- const initial = JSON.stringify({
183
- format: post?.format ?? "note",
184
- title: post?.title ?? "",
185
- body: post?.body ?? "",
186
- url: post?.url ?? "",
187
- quoteText: post?.quoteText ?? "",
188
- status: post?.status ?? "published",
189
- visibility: post?.visibility ?? "listed",
190
- pinned: post?.pinned === 1,
191
- rating: post?.rating ?? 0,
192
- collectionIds: postCollectionIds,
193
- mediaIds: mediaAttachments.map((m) => m.id),
194
- }).replace(/</g, "\\u003c");
195
-
196
- const media = JSON.stringify(
197
- mediaAttachments.map((m) => {
198
- const pUrl = getPublicUrlForProvider(
199
- m.provider,
200
- r2PublicUrl,
201
- s3PublicUrl,
202
- );
203
- const mediaUrl = getMediaUrl(m.storageKey, pUrl);
204
- const thumbUrl = getImageUrl(mediaUrl, imageTransformUrl, {
205
- width: 150,
206
- quality: 80,
207
- format: "auto",
208
- fit: "cover",
209
- });
210
- return {
211
- id: m.id,
212
- thumbUrl,
213
- alt: m.alt || m.originalName,
214
- };
215
- }),
216
- ).replace(/</g, "\\u003c");
217
-
218
- const collectionOptions = JSON.stringify(
219
- collections.map((col) => ({
220
- id: col.id,
221
- title: col.title,
222
- icon: col.icon,
223
- iconHtml: renderCollectionIcon(col.icon, { size: 18 }),
224
- })),
225
- ).replace(/</g, "\\u003c");
226
-
227
- const cancel = cancelHref ?? "/dash/posts";
228
-
229
- return (
230
- <jant-post-form
231
- labels={labels}
232
- initial={initial}
233
- action={action}
234
- cancel-href={cancel}
235
- media={media}
236
- collections={collectionOptions}
237
- media-picker-url="/dash/media/picker"
238
- is-edit={isEdit ? "true" : undefined}
239
- >
240
- <div class="flex flex-col gap-4 max-w-2xl">
241
- <div class="field">
242
- <div class="label skel-label"></div>
243
- <div class="input skel-input"></div>
244
- </div>
245
- <div class="field">
246
- <div class="label skel-label"></div>
247
- <div class="textarea skel-textarea"></div>
248
- </div>
249
- <div class="field">
250
- <div class="label skel-label"></div>
251
- <div class="input skel-input"></div>
252
- </div>
253
- <div class="flex gap-2">
254
- <div class="btn skel-input min-w-24"></div>
255
- <div class="btn-outline skel-input min-w-20"></div>
256
- </div>
257
- </div>
258
- </jant-post-form>
259
- );
260
- };
@@ -1,247 +0,0 @@
1
- /**
2
- * Dashboard Layout
3
- *
4
- * Layout for admin dashboard pages
5
- */
6
-
7
- import type { FC, PropsWithChildren } from "hono/jsx";
8
- import type { Context } from "hono";
9
- import { useLingui } from "@lingui/react/macro";
10
- import { BaseLayout, type ToastProps } from "./BaseLayout.js";
11
-
12
- export interface DashBreadcrumb {
13
- parent: string;
14
- parentHref: string;
15
- current: string;
16
- }
17
-
18
- export interface DashLayoutProps {
19
- c: Context;
20
- title: string;
21
- siteName: string;
22
- siteAvatarUrl?: string;
23
- currentPath?: string;
24
- breadcrumb?: DashBreadcrumb;
25
- toast?: ToastProps;
26
- }
27
-
28
- const AVATAR_COLORS = [
29
- "#737fab", // slate blue
30
- "#8d7dab", // muted violet
31
- "#ab7d8d", // dusty rose
32
- "#ab917d", // warm taupe
33
- "#7dab8d", // sage green
34
- "#7d9bab", // steel blue
35
- "#9a8d7d", // earth brown
36
- "#7dabab", // teal grey
37
- ];
38
-
39
- function hashString(str: string): number {
40
- let hash = 5381;
41
- for (let i = 0; i < str.length; i++) {
42
- hash = (hash * 33) ^ str.charCodeAt(i);
43
- }
44
- return Math.abs(hash);
45
- }
46
-
47
- function DashLayoutContent({
48
- siteName,
49
- siteAvatarUrl,
50
- currentPath,
51
- breadcrumb,
52
- children,
53
- }: PropsWithChildren<Omit<DashLayoutProps, "c" | "title">>) {
54
- const { t } = useLingui();
55
-
56
- const navClass = (match: RegExp) =>
57
- `dash-header-link ${currentPath && match.test(currentPath) ? "dash-header-link-active" : ""}`;
58
-
59
- return (
60
- <div class="min-h-screen">
61
- {/* Header */}
62
- <header class="dash-header">
63
- <div class="container dash-header-inner">
64
- <a href="/dash" class="dash-header-avatar-link">
65
- {siteAvatarUrl ? (
66
- <img src={siteAvatarUrl} alt="" class="dash-header-avatar" />
67
- ) : (
68
- <span
69
- class="dash-header-avatar dash-header-avatar-fallback"
70
- style={`background-color: ${AVATAR_COLORS[hashString(siteName) % AVATAR_COLORS.length]}`}
71
- >
72
- {siteName.charAt(0).toUpperCase()}
73
- </span>
74
- )}
75
- </a>
76
- <nav class="dash-header-nav">
77
- <a href="/dash" class={navClass(/^\/dash$/)}>
78
- {t({
79
- message: "Dashboard",
80
- comment: "@context: Dashboard navigation - dashboard home",
81
- })}
82
- </a>
83
- <span class="dash-header-nav-sep" aria-hidden="true">
84
- &middot;
85
- </span>
86
- <a href="/dash/pages" class={navClass(/^\/dash\/pages/)}>
87
- {t({
88
- message: "Pages",
89
- comment: "@context: Dashboard navigation - pages management",
90
- })}
91
- </a>
92
- <span class="dash-header-nav-sep" aria-hidden="true">
93
- &middot;
94
- </span>
95
- <a href="/dash/settings" class={navClass(/^\/dash\/settings/)}>
96
- {t({
97
- message: "Settings",
98
- comment: "@context: Dashboard navigation - site settings",
99
- })}
100
- </a>
101
- </nav>
102
-
103
- <div class="dash-header-right">
104
- <a
105
- href="/"
106
- class="dash-header-visit"
107
- target="_blank"
108
- aria-label={t({
109
- message: "Visit Blog",
110
- comment:
111
- "@context: Dashboard header link to visit the public blog",
112
- })}
113
- >
114
- <span
115
- class="dash-header-visit-icon"
116
- data-tooltip={t({
117
- message: "Visit Blog",
118
- comment:
119
- "@context: Dashboard header tooltip for visit blog icon on mobile",
120
- })}
121
- data-side="bottom"
122
- >
123
- <svg
124
- xmlns="http://www.w3.org/2000/svg"
125
- width="16"
126
- height="16"
127
- viewBox="0 0 24 24"
128
- fill="none"
129
- stroke="currentColor"
130
- stroke-width="2"
131
- stroke-linecap="round"
132
- stroke-linejoin="round"
133
- >
134
- <path d="M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6" />
135
- <path d="m21 3-9 9" />
136
- <path d="M15 3h6v6" />
137
- </svg>
138
- </span>
139
- <span class="dash-header-visit-text">
140
- {t({
141
- message: "Visit Blog",
142
- comment:
143
- "@context: Dashboard header link text to visit the public blog",
144
- })}
145
- <span class="ml-1" aria-hidden="true">
146
- {"\u2197"}
147
- </span>
148
- </span>
149
- </a>
150
-
151
- <div class="dropdown-menu">
152
- <button
153
- type="button"
154
- id="dash-menu-trigger"
155
- class="dash-header-menu-btn"
156
- aria-haspopup="menu"
157
- aria-controls="dash-menu"
158
- aria-expanded="false"
159
- aria-label={t({
160
- message: "Menu",
161
- comment: "@context: Dashboard header menu button",
162
- })}
163
- >
164
- <svg
165
- xmlns="http://www.w3.org/2000/svg"
166
- width="16"
167
- height="16"
168
- viewBox="0 0 24 24"
169
- fill="currentColor"
170
- >
171
- <circle cx="5" cy="12" r="2" />
172
- <circle cx="12" cy="12" r="2" />
173
- <circle cx="19" cy="12" r="2" />
174
- </svg>
175
- </button>
176
- <div
177
- id="dash-menu-popover"
178
- data-popover
179
- data-align="end"
180
- aria-hidden="true"
181
- >
182
- <div
183
- role="menu"
184
- id="dash-menu"
185
- aria-labelledby="dash-menu-trigger"
186
- >
187
- <a href="/signout" role="menuitem">
188
- {t({
189
- message: "Sign Out",
190
- comment: "@context: Dashboard menu item to sign out",
191
- })}
192
- </a>
193
- </div>
194
- </div>
195
- </div>
196
- </div>
197
- </div>
198
- </header>
199
-
200
- {breadcrumb && (
201
- <div class="container">
202
- <nav class="dash-breadcrumb">
203
- <a href={breadcrumb.parentHref} class="dash-breadcrumb-parent">
204
- {breadcrumb.parent}
205
- </a>
206
- <span class="dash-breadcrumb-sep">/</span>
207
- <span class="dash-breadcrumb-current">{breadcrumb.current}</span>
208
- </nav>
209
- </div>
210
- )}
211
-
212
- {/* Main */}
213
- <div class="container py-8">
214
- <main>{children}</main>
215
- </div>
216
- </div>
217
- );
218
- }
219
-
220
- export const DashLayout: FC<PropsWithChildren<DashLayoutProps>> = ({
221
- c,
222
- title,
223
- siteName,
224
- siteAvatarUrl,
225
- currentPath,
226
- breadcrumb,
227
- toast,
228
- children,
229
- }) => {
230
- return (
231
- <BaseLayout
232
- title={`${title} - ${siteName}`}
233
- c={c}
234
- toast={toast}
235
- isAuthenticated={true}
236
- >
237
- <DashLayoutContent
238
- siteName={siteName}
239
- siteAvatarUrl={siteAvatarUrl}
240
- currentPath={currentPath}
241
- breadcrumb={breadcrumb}
242
- >
243
- {children}
244
- </DashLayoutContent>
245
- </BaseLayout>
246
- );
247
- };
@@ -1,23 +0,0 @@
1
- /**
2
- * Single Page (Custom Page)
3
- *
4
- * Custom page view — clean centered content.
5
- */
6
-
7
- import type { FC } from "hono/jsx";
8
- import type { SinglePageProps } from "../../types.js";
9
-
10
- export const SinglePage: FC<SinglePageProps> = ({ page }) => {
11
- return (
12
- <article class="h-entry py-6" data-page="single-page">
13
- {page.title && (
14
- <h1 class="p-name text-2xl font-semibold mb-6">{page.title}</h1>
15
- )}
16
-
17
- <div
18
- class="e-content prose"
19
- dangerouslySetInnerHTML={{ __html: page.bodyHtml || "" }}
20
- />
21
- </article>
22
- );
23
- };
@@ -1,136 +0,0 @@
1
- /**
2
- * Thread View Component
3
- *
4
- * Displays a thread of posts with reply chain visualization
5
- */
6
-
7
- import type { FC } from "hono/jsx";
8
- import { useLingui } from "@lingui/react/macro";
9
- import type { Post } from "../../types.js";
10
- import * as sqid from "../../lib/sqid.js";
11
- import * as time from "../../lib/time.js";
12
-
13
- export interface ThreadViewProps {
14
- /** All posts in the thread, ordered by createdAt */
15
- posts: Post[];
16
- /** ID of the currently viewed post (to highlight) */
17
- currentPostId: number;
18
- }
19
-
20
- const ThreadPost: FC<{
21
- post: Post;
22
- isCurrent: boolean;
23
- isRoot: boolean;
24
- }> = ({ post, isCurrent, isRoot }) => {
25
- const { t } = useLingui();
26
- return (
27
- <article
28
- id={`post-${post.id}`}
29
- class={`h-entry p-4 rounded-lg border ${
30
- isCurrent
31
- ? "border-primary bg-primary/5 ring-2 ring-primary/20"
32
- : "border-border hover:border-muted-foreground/30"
33
- }`}
34
- >
35
- {post.title && (
36
- <h2 class="p-name text-lg font-medium mb-2">
37
- <a
38
- href={`${post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`}`}
39
- class="u-url hover:underline"
40
- >
41
- {post.title}
42
- </a>
43
- </h2>
44
- )}
45
-
46
- <div
47
- class="e-content prose prose-sm"
48
- dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
49
- />
50
-
51
- <footer class="mt-3 flex items-center gap-3 text-sm text-muted-foreground">
52
- <time
53
- class="dt-published"
54
- datetime={time.toISOString(post.publishedAt)}
55
- >
56
- {time.formatDate(post.publishedAt)}
57
- </time>
58
- {isRoot && (
59
- <span class="text-xs">
60
- {t({
61
- message: "Thread start",
62
- comment: "@context: Thread view indicator - first post in thread",
63
- })}
64
- </span>
65
- )}
66
- {!isCurrent && (
67
- <a
68
- href={`${post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`}`}
69
- class="text-xs hover:underline"
70
- >
71
- {t({
72
- message: "Permalink",
73
- comment: "@context: Link to individual post in thread",
74
- })}
75
- </a>
76
- )}
77
- </footer>
78
- </article>
79
- );
80
- };
81
-
82
- export const ThreadView: FC<ThreadViewProps> = ({ posts, currentPostId }) => {
83
- const { t } = useLingui();
84
- if (posts.length === 0) {
85
- return null;
86
- }
87
-
88
- const rootPost = posts[0];
89
- const isThread = posts.length > 1;
90
-
91
- // Single post, no thread
92
- if (!isThread) {
93
- return (
94
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Early return for empty array at line 73 guarantees posts[0] exists
95
- <ThreadPost post={rootPost!} isCurrent={true} isRoot={false} />
96
- );
97
- }
98
-
99
- const threadLabel =
100
- posts.length === 1
101
- ? t({
102
- message: "Thread with 1 post",
103
- comment: "@context: Thread view header - single post",
104
- })
105
- : t({
106
- message: "Thread with {count} posts",
107
- comment: "@context: Thread view header - multiple posts",
108
- values: { count: String(posts.length) },
109
- });
110
-
111
- return (
112
- <div class="thread-view">
113
- <div class="mb-4 text-sm text-muted-foreground">{threadLabel}</div>
114
-
115
- <div class="flex flex-col gap-3">
116
- {posts.map((post, index) => (
117
- <div key={post.id} class="relative">
118
- {/* Connection line */}
119
- {index > 0 && (
120
- <div class="absolute left-6 -top-3 w-0.5 h-3 bg-border" />
121
- )}
122
- {index < posts.length - 1 && (
123
- <div class="absolute left-6 -bottom-3 w-0.5 h-3 bg-border" />
124
- )}
125
-
126
- <ThreadPost
127
- post={post}
128
- isCurrent={post.id === currentPostId}
129
- isRoot={index === 0}
130
- />
131
- </div>
132
- ))}
133
- </div>
134
- </div>
135
- );
136
- };