@jant/core 0.3.35 → 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 (307) 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/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -1,257 +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 { SettingsNav } from "../../ui/dash/settings/SettingsNav.js";
15
- import { dsRedirect } from "../../lib/sse.js";
16
- import { RedirectTypeSchema, parseValidated } from "../../lib/schemas.js";
17
-
18
- type Env = { Bindings: Bindings; Variables: AppVariables };
19
-
20
- const CreateRedirectBody = z.object({
21
- fromPath: z.string().min(1),
22
- toPath: z.string().min(1),
23
- type: RedirectTypeSchema,
24
- });
25
-
26
- export const redirectsRoutes = new Hono<Env>();
27
-
28
- function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
29
- const { t } = useLingui();
30
-
31
- return (
32
- <>
33
- <SettingsNav currentTab="redirects" />
34
-
35
- <div class="flex items-center justify-between mb-6">
36
- <h2 class="text-lg font-medium">
37
- {t({
38
- message: "Redirects",
39
- comment: "@context: Settings section heading",
40
- })}
41
- </h2>
42
- <a href="/dash/settings/redirects/new" class="btn">
43
- {t({
44
- message: "New Redirect",
45
- comment: "@context: Button to create new redirect",
46
- })}
47
- </a>
48
- </div>
49
-
50
- {redirects.length === 0 ? (
51
- <EmptyState
52
- message={t({
53
- message: "No redirects configured.",
54
- comment: "@context: Empty state message",
55
- })}
56
- ctaText={t({
57
- message: "New Redirect",
58
- comment: "@context: Button to create new redirect",
59
- })}
60
- ctaHref="/dash/settings/redirects/new"
61
- />
62
- ) : (
63
- <div class="flex flex-col divide-y">
64
- {redirects.map((r) => (
65
- <ListItemRow
66
- key={r.id}
67
- actions={
68
- <ActionButtons
69
- deleteAction={`/dash/settings/redirects/${r.id}/delete`}
70
- deleteLabel={t({
71
- message: "Delete",
72
- comment: "@context: Button to delete redirect",
73
- })}
74
- />
75
- }
76
- >
77
- <div class="flex items-center gap-2">
78
- <code class="text-sm bg-muted px-1 rounded">{r.fromPath}</code>
79
- <span class="text-muted-foreground">→</span>
80
- <code class="text-sm bg-muted px-1 rounded">{r.toPath}</code>
81
- <span class="badge-outline">{r.type}</span>
82
- </div>
83
- </ListItemRow>
84
- ))}
85
- </div>
86
- )}
87
- </>
88
- );
89
- }
90
-
91
- function NewRedirectContent() {
92
- const { t } = useLingui();
93
-
94
- return (
95
- <>
96
- <SettingsNav currentTab="redirects" />
97
-
98
- <h2 class="text-lg font-medium mb-6">
99
- {t({ message: "New Redirect", comment: "@context: Page heading" })}
100
- </h2>
101
-
102
- <form
103
- data-signals="{fromPath: '', toPath: '', type: '301'}"
104
- data-on:submit__prevent="@post('/dash/settings/redirects')"
105
- data-indicator="_loading"
106
- class="flex flex-col gap-4 max-w-lg"
107
- >
108
- <div class="field">
109
- <label class="label">
110
- {t({
111
- message: "From Path",
112
- comment: "@context: Redirect form field",
113
- })}
114
- </label>
115
- <input
116
- type="text"
117
- data-bind="fromPath"
118
- class="input"
119
- placeholder="/old-path"
120
- required
121
- />
122
- <p class="text-xs text-muted-foreground mt-1">
123
- {t({
124
- message: "The path to redirect from",
125
- comment: "@context: Redirect from path help text",
126
- })}
127
- </p>
128
- </div>
129
-
130
- <div class="field">
131
- <label class="label">
132
- {t({
133
- message: "To Path",
134
- comment: "@context: Redirect form field",
135
- })}
136
- </label>
137
- <input
138
- type="text"
139
- data-bind="toPath"
140
- class="input"
141
- placeholder="/new-path or https://..."
142
- required
143
- />
144
- <p class="text-xs text-muted-foreground mt-1">
145
- {t({
146
- message: "The destination path or URL",
147
- comment: "@context: Redirect to path help text",
148
- })}
149
- </p>
150
- </div>
151
-
152
- <div class="field">
153
- <label class="label">
154
- {t({ message: "Type", comment: "@context: Redirect form field" })}
155
- </label>
156
- <select data-bind="type" class="select">
157
- <option value="301">
158
- {t({
159
- message: "301 (Permanent)",
160
- comment: "@context: Redirect type option",
161
- })}
162
- </option>
163
- <option value="302">
164
- {t({
165
- message: "302 (Temporary)",
166
- comment: "@context: Redirect type option",
167
- })}
168
- </option>
169
- </select>
170
- </div>
171
-
172
- <div class="flex gap-2">
173
- <button type="submit" class="btn" data-attr:disabled="$_loading">
174
- <svg
175
- data-show="$_loading"
176
- style="display:none"
177
- class="animate-spin size-4"
178
- xmlns="http://www.w3.org/2000/svg"
179
- viewBox="0 0 24 24"
180
- fill="none"
181
- stroke="currentColor"
182
- stroke-width="2"
183
- stroke-linecap="round"
184
- stroke-linejoin="round"
185
- role="status"
186
- >
187
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
188
- </svg>
189
- {t({
190
- message: "Create Redirect",
191
- comment: "@context: Button to save new redirect",
192
- })}
193
- </button>
194
- <a href="/dash/settings/redirects" class="btn-outline">
195
- {t({
196
- message: "Cancel",
197
- comment: "@context: Button to cancel form",
198
- })}
199
- </a>
200
- </div>
201
- </form>
202
- </>
203
- );
204
- }
205
-
206
- // List redirects
207
- redirectsRoutes.get("/", async (c) => {
208
- const siteName = c.var.appConfig.siteName;
209
- const redirects = await c.var.services.redirects.list();
210
-
211
- return c.html(
212
- <DashLayout
213
- c={c}
214
- title="Settings"
215
- siteName={siteName}
216
- currentPath="/dash/settings"
217
- >
218
- <RedirectsListContent redirects={redirects} />
219
- </DashLayout>,
220
- );
221
- });
222
-
223
- // New redirect form
224
- redirectsRoutes.get("/new", async (c) => {
225
- const siteName = c.var.appConfig.siteName;
226
-
227
- return c.html(
228
- <DashLayout
229
- c={c}
230
- title="Settings"
231
- siteName={siteName}
232
- currentPath="/dash/settings"
233
- >
234
- <NewRedirectContent />
235
- </DashLayout>,
236
- );
237
- });
238
-
239
- // Create redirect
240
- redirectsRoutes.post("/", async (c) => {
241
- const body = parseValidated(CreateRedirectBody, await c.req.json());
242
-
243
- const type = parseInt(body.type, 10) as 301 | 302;
244
- await c.var.services.redirects.create(body.fromPath, body.toPath, type);
245
-
246
- return dsRedirect("/dash/settings/redirects");
247
- });
248
-
249
- // Delete redirect
250
- redirectsRoutes.post("/:id/delete", async (c) => {
251
- const id = parseInt(c.req.param("id"), 10);
252
- if (!isNaN(id)) {
253
- await c.var.services.redirects.delete(id);
254
- }
255
-
256
- return dsRedirect("/dash/settings/redirects");
257
- });
@@ -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
- });
@@ -1,298 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { createTestDatabase } from "../../__tests__/helpers/db.js";
3
- import { createPageService } from "../page.js";
4
- import { createPostService } from "../post.js";
5
- import { createNavItemService } from "../navigation.js";
6
- import { createPathRegistryService } from "../path-registry.js";
7
- import { ValidationError, ConflictError } from "../../lib/errors.js";
8
- import type { Database } from "../../db/index.js";
9
-
10
- describe("PageService", () => {
11
- let db: Database;
12
- let pageService: ReturnType<typeof createPageService>;
13
- let postService: ReturnType<typeof createPostService>;
14
- let navItemService: ReturnType<typeof createNavItemService>;
15
- let pathRegistry: ReturnType<typeof createPathRegistryService>;
16
-
17
- beforeEach(() => {
18
- const testDb = createTestDatabase();
19
- db = testDb.db as unknown as Database;
20
- pathRegistry = createPathRegistryService(db);
21
- pageService = createPageService(db, pathRegistry);
22
- postService = createPostService(db, pathRegistry);
23
- navItemService = createNavItemService(db);
24
- });
25
-
26
- describe("listNotInNav", () => {
27
- it("returns all pages when none are in navigation", async () => {
28
- await pageService.create({ slug: "about", title: "About" });
29
- await pageService.create({ slug: "contact", title: "Contact" });
30
-
31
- const pages = await pageService.listNotInNav();
32
- expect(pages).toHaveLength(2);
33
- });
34
-
35
- it("excludes pages that have a nav item", async () => {
36
- const aboutPage = await pageService.create({
37
- slug: "about",
38
- title: "About",
39
- });
40
- await pageService.create({ slug: "contact", title: "Contact" });
41
-
42
- // Add "About" to navigation
43
- await navItemService.create({
44
- type: "page",
45
- label: "About",
46
- url: "/about",
47
- pageId: aboutPage.id,
48
- });
49
-
50
- const pages = await pageService.listNotInNav();
51
- expect(pages).toHaveLength(1);
52
- expect(pages[0]?.slug).toBe("contact");
53
- });
54
-
55
- it("returns empty array when all pages are in navigation", async () => {
56
- const aboutPage = await pageService.create({
57
- slug: "about",
58
- title: "About",
59
- });
60
-
61
- await navItemService.create({
62
- type: "page",
63
- label: "About",
64
- url: "/about",
65
- pageId: aboutPage.id,
66
- });
67
-
68
- const pages = await pageService.listNotInNav();
69
- expect(pages).toHaveLength(0);
70
- });
71
-
72
- it("returns empty array when no pages exist", async () => {
73
- const pages = await pageService.listNotInNav();
74
- expect(pages).toHaveLength(0);
75
- });
76
-
77
- it("is not affected by link-type nav items (no pageId)", async () => {
78
- await pageService.create({ slug: "about", title: "About" });
79
-
80
- // Link-type nav items have no pageId
81
- await navItemService.create({
82
- type: "link",
83
- label: "External",
84
- url: "https://example.com",
85
- });
86
-
87
- const pages = await pageService.listNotInNav();
88
- expect(pages).toHaveLength(1);
89
- });
90
-
91
- it("returns multiple pages correctly", async () => {
92
- await pageService.create({ slug: "first", title: "First" });
93
- await pageService.create({ slug: "second", title: "Second" });
94
- await pageService.create({ slug: "third", title: "Third" });
95
-
96
- // Add one to nav
97
- const pages = await pageService.list();
98
- await navItemService.create({
99
- type: "page",
100
- label: "Second",
101
- url: "/second",
102
- pageId: (
103
- pages.find((p) => p.slug === "second") as (typeof pages)[number]
104
- ).id,
105
- });
106
-
107
- const notInNav = await pageService.listNotInNav();
108
- expect(notInNav).toHaveLength(2);
109
- const slugs = notInNav.map((p) => p.slug);
110
- expect(slugs).toContain("first");
111
- expect(slugs).toContain("third");
112
- expect(slugs).not.toContain("second");
113
- });
114
- });
115
-
116
- describe("update nav item sync", () => {
117
- it("syncs nav item label when page title changes", async () => {
118
- const page = await pageService.create({
119
- slug: "about",
120
- title: "About",
121
- });
122
- await navItemService.create({
123
- type: "page",
124
- label: "About",
125
- url: "/about",
126
- pageId: page.id,
127
- });
128
-
129
- await pageService.update(page.id, { title: "About Us" });
130
-
131
- const navs = await navItemService.list();
132
- expect(navs).toHaveLength(1);
133
- expect(navs[0]?.label).toBe("About Us");
134
- });
135
-
136
- it("syncs nav item url when page slug changes", async () => {
137
- const page = await pageService.create({
138
- slug: "about",
139
- title: "About",
140
- });
141
- await navItemService.create({
142
- type: "page",
143
- label: "About",
144
- url: "/about",
145
- pageId: page.id,
146
- });
147
-
148
- await pageService.update(page.id, { slug: "about-us" });
149
-
150
- const navs = await navItemService.list();
151
- expect(navs).toHaveLength(1);
152
- expect(navs[0]?.url).toBe("/about-us");
153
- });
154
-
155
- it("syncs both label and url when title and slug change together", async () => {
156
- const page = await pageService.create({
157
- slug: "about",
158
- title: "About",
159
- });
160
- await navItemService.create({
161
- type: "page",
162
- label: "About",
163
- url: "/about",
164
- pageId: page.id,
165
- });
166
-
167
- await pageService.update(page.id, {
168
- title: "About Our Company",
169
- slug: "about-our-company",
170
- });
171
-
172
- const navs = await navItemService.list();
173
- expect(navs).toHaveLength(1);
174
- expect(navs[0]?.label).toBe("About Our Company");
175
- expect(navs[0]?.url).toBe("/about-our-company");
176
- });
177
-
178
- it("does not change nav item label when title is unchanged", async () => {
179
- const page = await pageService.create({
180
- slug: "about",
181
- title: "About",
182
- });
183
- await navItemService.create({
184
- type: "page",
185
- label: "Custom Label",
186
- url: "/about",
187
- pageId: page.id,
188
- });
189
-
190
- // Update body only, not title
191
- await pageService.update(page.id, { body: "New content" });
192
-
193
- const navs = await navItemService.list();
194
- expect(navs[0]?.label).toBe("Custom Label");
195
- });
196
-
197
- it("does not affect nav items for other pages", async () => {
198
- const page1 = await pageService.create({
199
- slug: "about",
200
- title: "About",
201
- });
202
- const page2 = await pageService.create({
203
- slug: "contact",
204
- title: "Contact",
205
- });
206
- await navItemService.create({
207
- type: "page",
208
- label: "About",
209
- url: "/about",
210
- pageId: page1.id,
211
- });
212
- await navItemService.create({
213
- type: "page",
214
- label: "Contact",
215
- url: "/contact",
216
- pageId: page2.id,
217
- });
218
-
219
- await pageService.update(page1.id, { title: "About Us" });
220
-
221
- const navs = await navItemService.list();
222
- const aboutNav = navs.find((n) => n.pageId === page1.id);
223
- const contactNav = navs.find((n) => n.pageId === page2.id);
224
- expect(aboutNav?.label).toBe("About Us");
225
- expect(contactNav?.label).toBe("Contact");
226
- });
227
- });
228
-
229
- describe("path registry integration", () => {
230
- it("rejects reserved slug on create", async () => {
231
- await expect(
232
- pageService.create({ slug: "dash", title: "Dashboard" }),
233
- ).rejects.toThrow(ValidationError);
234
- });
235
-
236
- it("rejects slug that conflicts with a post path", async () => {
237
- await postService.create({
238
- format: "note",
239
- body: "test",
240
- path: "about",
241
- });
242
-
243
- await expect(
244
- pageService.create({ slug: "about", title: "About" }),
245
- ).rejects.toThrow(ConflictError);
246
- });
247
-
248
- it("rejects reserved slug on update", async () => {
249
- const page = await pageService.create({
250
- slug: "about",
251
- title: "About",
252
- });
253
-
254
- await expect(
255
- pageService.update(page.id, { slug: "api" }),
256
- ).rejects.toThrow(ValidationError);
257
- });
258
-
259
- it("rejects slug update that conflicts with another entity", async () => {
260
- const page = await pageService.create({
261
- slug: "about",
262
- title: "About",
263
- });
264
- await postService.create({
265
- format: "note",
266
- body: "test",
267
- path: "contact",
268
- });
269
-
270
- await expect(
271
- pageService.update(page.id, { slug: "contact" }),
272
- ).rejects.toThrow(ConflictError);
273
- });
274
-
275
- it("releases path on delete", async () => {
276
- const page = await pageService.create({
277
- slug: "about",
278
- title: "About",
279
- });
280
-
281
- await pageService.delete(page.id);
282
-
283
- expect(await pathRegistry.isAvailable("about")).toBe(true);
284
- });
285
-
286
- it("releases old slug and claims new slug on update", async () => {
287
- const page = await pageService.create({
288
- slug: "about",
289
- title: "About",
290
- });
291
-
292
- await pageService.update(page.id, { slug: "about-us" });
293
-
294
- expect(await pathRegistry.isAvailable("about")).toBe(true);
295
- expect(await pathRegistry.isAvailable("about-us")).toBe(false);
296
- });
297
- });
298
- });