@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,240 +0,0 @@
1
- /**
2
- * Dashboard Appearance Routes
3
- *
4
- * Sub-pages: Navigation (default), Color Theme, Font Theme, Advanced (Custom CSS)
5
- */
6
-
7
- import { Hono } from "hono";
8
- import { msg } from "@lingui/core/macro";
9
- import type { Bindings } from "../../types.js";
10
- import type { AppVariables } from "../../types/app-context.js";
11
- import { DashLayout } from "../../ui/layouts/DashLayout.js";
12
- import { dsRedirect, dsToast } from "../../lib/sse.js";
13
- import { getI18n } from "../../i18n/index.js";
14
- import { SETTINGS_KEYS } from "../../lib/constants.js";
15
- import { getAvailableThemes } from "../../lib/theme.js";
16
- import { BUILTIN_FONT_THEMES } from "../../ui/font-themes.js";
17
- import { ColorThemeContent } from "../../ui/dash/appearance/ColorThemeContent.js";
18
- import { FontThemeContent } from "../../ui/dash/appearance/FontThemeContent.js";
19
- import { NavigationContent } from "../../ui/dash/appearance/NavigationContent.js";
20
- import { AdvancedContent } from "../../ui/dash/appearance/AdvancedContent.js";
21
-
22
- type Env = { Bindings: Bindings; Variables: AppVariables };
23
-
24
- export const appearanceRoutes = new Hono<Env>();
25
-
26
- // ===========================================================================
27
- // Navigation (default tab)
28
- // ===========================================================================
29
-
30
- appearanceRoutes.get("/", async (c) => {
31
- const [navItems, availablePages] = await Promise.all([
32
- c.var.services.navItems.list(),
33
- c.var.services.pages.listNotInNav(),
34
- ]);
35
- const siteName = c.var.appConfig.siteName;
36
- const headerNavMaxVisible = c.var.appConfig.headerNavMaxVisible;
37
- const homeDefaultView = c.var.appConfig.homeDefaultView;
38
-
39
- return c.html(
40
- <DashLayout
41
- c={c}
42
- title="Appearance"
43
- siteName={siteName}
44
- currentPath="/dash/appearance"
45
- >
46
- <NavigationContent
47
- navItems={navItems}
48
- availablePages={availablePages}
49
- headerNavMaxVisible={headerNavMaxVisible}
50
- homeDefaultView={homeDefaultView}
51
- siteName={siteName}
52
- />
53
- </DashLayout>,
54
- );
55
- });
56
-
57
- // ===========================================================================
58
- // Nav max visible links
59
- // ===========================================================================
60
-
61
- appearanceRoutes.post("/nav-max-visible", async (c) => {
62
- const body = await c.req.json<{ value: number }>();
63
- const { settings } = c.var.services;
64
-
65
- const navMax = Math.max(0, Math.min(5, body.value ?? 3));
66
- if (navMax !== 3) {
67
- await settings.set("HEADER_NAV_MAX_VISIBLE", String(navMax));
68
- } else {
69
- await settings.remove("HEADER_NAV_MAX_VISIBLE");
70
- }
71
-
72
- return c.json({ ok: true });
73
- });
74
-
75
- // ===========================================================================
76
- // Home default view
77
- // ===========================================================================
78
-
79
- appearanceRoutes.post("/home-default-view", async (c) => {
80
- const body = await c.req.json<{ value: string }>();
81
- const { settings } = c.var.services;
82
-
83
- if (body.value === "featured") {
84
- await settings.set("HOME_DEFAULT_VIEW", "featured");
85
- } else {
86
- await settings.remove("HOME_DEFAULT_VIEW");
87
- }
88
-
89
- return c.json({ ok: true });
90
- });
91
-
92
- // ===========================================================================
93
- // Color Theme
94
- // ===========================================================================
95
-
96
- appearanceRoutes.get("/color", async (c) => {
97
- const siteName = c.var.appConfig.siteName;
98
- const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
99
- const currentThemeId =
100
- c.var.allSettings[SETTINGS_KEYS.THEME] ?? defaultThemeId;
101
- const themes = getAvailableThemes();
102
- const saved = c.req.query("saved") !== undefined;
103
-
104
- return c.html(
105
- <DashLayout
106
- c={c}
107
- title="Appearance"
108
- siteName={siteName}
109
- currentPath="/dash/appearance"
110
- toast={saved ? { message: "Theme saved successfully." } : undefined}
111
- >
112
- <ColorThemeContent themes={themes} currentThemeId={currentThemeId} />
113
- </DashLayout>,
114
- );
115
- });
116
-
117
- appearanceRoutes.post("/color", async (c) => {
118
- const i18n = getI18n(c);
119
- const body = await c.req.json<{ theme: string }>();
120
- const { settings } = c.var.services;
121
- const themes = getAvailableThemes();
122
-
123
- const validTheme = themes.find((t) => t.id === body.theme);
124
- if (!validTheme) {
125
- return dsToast(
126
- i18n._(
127
- msg({
128
- message: "Invalid theme selected.",
129
- comment: "@context: Error toast when selected theme is not valid",
130
- }),
131
- ),
132
- "error",
133
- );
134
- }
135
-
136
- const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
137
- if (validTheme.id === defaultThemeId) {
138
- await settings.remove(SETTINGS_KEYS.THEME);
139
- } else {
140
- await settings.set(SETTINGS_KEYS.THEME, validTheme.id);
141
- }
142
-
143
- return dsRedirect("/dash/appearance/color?saved");
144
- });
145
-
146
- // ===========================================================================
147
- // Font Theme
148
- // ===========================================================================
149
-
150
- appearanceRoutes.get("/fonts", async (c) => {
151
- const siteName = c.var.appConfig.siteName;
152
- const currentFontThemeId = c.var.allSettings["FONT_THEME"] ?? "default";
153
- const saved = c.req.query("saved") !== undefined;
154
-
155
- return c.html(
156
- <DashLayout
157
- c={c}
158
- title="Appearance"
159
- siteName={siteName}
160
- currentPath="/dash/appearance"
161
- toast={saved ? { message: "Font theme saved successfully." } : undefined}
162
- >
163
- <FontThemeContent
164
- fontThemes={BUILTIN_FONT_THEMES}
165
- currentFontThemeId={currentFontThemeId}
166
- />
167
- </DashLayout>,
168
- );
169
- });
170
-
171
- appearanceRoutes.post("/font-theme", async (c) => {
172
- const i18n = getI18n(c);
173
- const body = await c.req.json<{ fontTheme: string }>();
174
- const { settings } = c.var.services;
175
-
176
- const validFont = BUILTIN_FONT_THEMES.find((f) => f.id === body.fontTheme);
177
- if (!validFont) {
178
- return dsToast(
179
- i18n._(
180
- msg({
181
- message: "Invalid font theme selected.",
182
- comment:
183
- "@context: Error toast when selected font theme is not valid",
184
- }),
185
- ),
186
- "error",
187
- );
188
- }
189
-
190
- if (validFont.id === "default") {
191
- await settings.remove("FONT_THEME");
192
- } else {
193
- await settings.set("FONT_THEME", validFont.id);
194
- }
195
-
196
- return dsRedirect("/dash/appearance/fonts?saved");
197
- });
198
-
199
- // ===========================================================================
200
- // Advanced (Custom CSS)
201
- // ===========================================================================
202
-
203
- appearanceRoutes.get("/advanced", async (c) => {
204
- const siteName = c.var.appConfig.siteName;
205
- const customCSS = c.var.allSettings[SETTINGS_KEYS.CUSTOM_CSS] ?? "";
206
-
207
- return c.html(
208
- <DashLayout
209
- c={c}
210
- title="Appearance"
211
- siteName={siteName}
212
- currentPath="/dash/appearance"
213
- >
214
- <AdvancedContent customCSS={customCSS} />
215
- </DashLayout>,
216
- );
217
- });
218
-
219
- appearanceRoutes.post("/custom-css", async (c) => {
220
- const i18n = getI18n(c);
221
- const body = await c.req.json<{ customCSS: string }>();
222
- const { settings } = c.var.services;
223
-
224
- const css = body.customCSS?.trim() ?? "";
225
-
226
- if (css) {
227
- await settings.set(SETTINGS_KEYS.CUSTOM_CSS, css);
228
- } else {
229
- await settings.remove(SETTINGS_KEYS.CUSTOM_CSS);
230
- }
231
-
232
- return dsToast(
233
- i18n._(
234
- msg({
235
- message: "Custom CSS saved successfully.",
236
- comment: "@context: Toast after saving custom CSS",
237
- }),
238
- ),
239
- );
240
- });
@@ -1,211 +0,0 @@
1
- /**
2
- * Dashboard Collections Routes
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 { DashLayout } from "../../ui/layouts/DashLayout.js";
9
- import { DangerZone } from "../../ui/dash/index.js";
10
- import { dsRedirect } from "../../lib/sse.js";
11
- import {
12
- CreateCollectionSchema,
13
- UpdateCollectionSchema,
14
- parseValidated,
15
- } from "../../lib/schemas.js";
16
- import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
17
- import { slugify } from "../../lib/url.js";
18
- import { CollectionsListContent } from "../../ui/dash/collections/CollectionsListContent.js";
19
- import { CollectionForm } from "../../ui/dash/collections/CollectionForm.js";
20
- import { ViewCollectionContent } from "../../ui/dash/collections/ViewCollectionContent.js";
21
- import { IconPickerGrid } from "../../ui/dash/collections/IconPickerGrid.js";
22
-
23
- type Env = { Bindings: Bindings; Variables: AppVariables };
24
-
25
- export const collectionsRoutes = new Hono<Env>();
26
-
27
- // List collections
28
- collectionsRoutes.get("/", async (c) => {
29
- const siteName = c.var.appConfig.siteName;
30
- const [collections, dividers, postCounts] = await Promise.all([
31
- c.var.services.collections.list(),
32
- c.var.services.collections.listDividers(),
33
- c.var.services.collections.getPostCounts(),
34
- ]);
35
-
36
- return c.html(
37
- <DashLayout
38
- c={c}
39
- title="Collections"
40
- siteName={siteName}
41
- currentPath="/dash/collections"
42
- >
43
- <CollectionsListContent
44
- collections={collections}
45
- dividers={dividers}
46
- postCounts={postCounts}
47
- />
48
- </DashLayout>,
49
- );
50
- });
51
-
52
- // New collection form
53
- collectionsRoutes.get("/new", async (c) => {
54
- const siteName = c.var.appConfig.siteName;
55
-
56
- return c.html(
57
- <DashLayout
58
- c={c}
59
- title="New Collection"
60
- siteName={siteName}
61
- currentPath="/dash/collections"
62
- >
63
- <CollectionForm />
64
- </DashLayout>,
65
- );
66
- });
67
-
68
- // Create collection
69
- collectionsRoutes.post("/", async (c) => {
70
- const wantsJson = c.req.header("Accept")?.includes("application/json");
71
- const raw = await c.req.json();
72
- const body = parseValidated(CreateCollectionSchema, {
73
- ...raw,
74
- slug: raw.slug || slugify(raw.title ?? ""),
75
- });
76
-
77
- const collection = await c.var.services.collections.create({
78
- title: body.title,
79
- slug: body.slug,
80
- description: body.description || undefined,
81
- icon: body.icon || undefined,
82
- sortOrder: body.sortOrder || undefined,
83
- });
84
-
85
- const redirectUrl = `/dash/collections/${collection.id}`;
86
- if (wantsJson) {
87
- return c.json({ status: "redirect" as const, url: redirectUrl });
88
- }
89
-
90
- return dsRedirect(redirectUrl);
91
- });
92
-
93
- // Reorder collections (accepts prefixed items)
94
- collectionsRoutes.post("/reorder", async (c) => {
95
- const body = await c.req.json<{ items?: string[]; ids?: number[] }>();
96
-
97
- if (body.items) {
98
- await c.var.services.collections.reorderAll(body.items);
99
- } else if (body.ids) {
100
- // Backward compat: plain numeric IDs
101
- await c.var.services.collections.reorder(body.ids);
102
- }
103
-
104
- return c.json({ success: true });
105
- });
106
-
107
- // Create divider
108
- collectionsRoutes.post("/dividers", async (c) => {
109
- await c.var.services.collections.createDivider();
110
- return dsRedirect("/dash/collections");
111
- });
112
-
113
- // Delete divider
114
- collectionsRoutes.post("/dividers/:id/delete", async (c) => {
115
- const id = parseInt(c.req.param("id"), 10);
116
- if (!isNaN(id)) {
117
- await c.var.services.collections.deleteDivider(id);
118
- }
119
- return dsRedirect("/dash/collections");
120
- });
121
-
122
- // Icon picker grid (HTML fragment)
123
- collectionsRoutes.get("/icons", (c) => {
124
- return c.html(<IconPickerGrid />);
125
- });
126
-
127
- // View single collection
128
- collectionsRoutes.get("/:id", async (c) => {
129
- const id = parseInt(c.req.param("id"), 10);
130
- if (isNaN(id)) return c.notFound();
131
-
132
- const collection = await c.var.services.collections.getById(id);
133
- if (!collection) return c.notFound();
134
-
135
- const rawPosts = await c.var.services.posts.list({ collectionId: id });
136
- const ctx = createMediaContext(c.var.appConfig);
137
- const posts = toPostViewsFromPosts(rawPosts, ctx);
138
- const siteName = c.var.appConfig.siteName;
139
-
140
- return c.html(
141
- <DashLayout
142
- c={c}
143
- title={collection.title}
144
- siteName={siteName}
145
- currentPath="/dash/collections"
146
- >
147
- <ViewCollectionContent collection={collection} posts={posts} />
148
- </DashLayout>,
149
- );
150
- });
151
-
152
- // Edit collection form
153
- collectionsRoutes.get("/:id/edit", async (c) => {
154
- const id = parseInt(c.req.param("id"), 10);
155
- if (isNaN(id)) return c.notFound();
156
-
157
- const collection = await c.var.services.collections.getById(id);
158
- if (!collection) return c.notFound();
159
-
160
- const siteName = c.var.appConfig.siteName;
161
-
162
- return c.html(
163
- <DashLayout
164
- c={c}
165
- title={`Edit: ${collection.title}`}
166
- siteName={siteName}
167
- currentPath="/dash/collections"
168
- >
169
- <CollectionForm collection={collection} isEdit />
170
- <DangerZone
171
- actionLabel="Delete Collection"
172
- formAction={`/dash/collections/${collection.id}/delete`}
173
- confirmMessage="Are you sure you want to delete this collection?"
174
- />
175
- </DashLayout>,
176
- );
177
- });
178
-
179
- // Update collection
180
- collectionsRoutes.post("/:id", async (c) => {
181
- const id = parseInt(c.req.param("id"), 10);
182
- if (isNaN(id)) return c.notFound();
183
-
184
- const wantsJson = c.req.header("Accept")?.includes("application/json");
185
- const body = parseValidated(UpdateCollectionSchema, await c.req.json());
186
-
187
- await c.var.services.collections.update(id, {
188
- title: body.title,
189
- slug: body.slug,
190
- description: body.description || null,
191
- icon: body.icon || null,
192
- sortOrder: body.sortOrder || undefined,
193
- });
194
-
195
- const redirectUrl = `/dash/collections/${id}`;
196
- if (wantsJson) {
197
- return c.json({ status: "redirect" as const, url: redirectUrl });
198
- }
199
-
200
- return dsRedirect(redirectUrl);
201
- });
202
-
203
- // Delete collection
204
- collectionsRoutes.post("/:id/delete", async (c) => {
205
- const id = parseInt(c.req.param("id"), 10);
206
- if (isNaN(id)) return c.notFound();
207
-
208
- await c.var.services.collections.delete(id);
209
-
210
- return dsRedirect("/dash/collections");
211
- });
@@ -1,103 +0,0 @@
1
- /**
2
- * Dashboard Index Route
3
- *
4
- * Example of using @lingui/react/macro with Hono JSX!
5
- */
6
-
7
- import { Hono } from "hono";
8
- import { Trans, useLingui } from "@lingui/react/macro";
9
- import type { Bindings } from "../../types.js";
10
- import type { AppVariables } from "../../types/app-context.js";
11
- import { DashLayout } from "../../ui/layouts/DashLayout.js";
12
-
13
- type Env = { Bindings: Bindings; Variables: AppVariables };
14
-
15
- export const dashIndexRoutes = new Hono<Env>();
16
-
17
- /**
18
- * Dashboard content component
19
- * Uses useLingui() from @lingui/react/macro - works with Hono JSX!
20
- */
21
- function DashboardContent({
22
- publishedCount,
23
- draftCount,
24
- }: {
25
- publishedCount: number;
26
- draftCount: number;
27
- }) {
28
- // 🎉 Single layer! Just like React!
29
- const { t } = useLingui();
30
-
31
- return (
32
- <>
33
- <h1 class="text-2xl font-semibold mb-6">
34
- {t({
35
- message: "Dashboard",
36
- comment: "@context: Dashboard main heading",
37
- })}
38
- </h1>
39
-
40
- <div class="grid gap-4 md:grid-cols-3 mb-6">
41
- <div class="p-4 border rounded">
42
- <p class="text-sm text-muted-foreground">
43
- {t({
44
- message: "Published",
45
- comment: "@context: Post status label",
46
- })}
47
- </p>
48
- <p class="text-3xl font-bold">{publishedCount}</p>
49
- </div>
50
-
51
- <div class="p-4 border rounded">
52
- <p class="text-sm text-muted-foreground">
53
- {t({ message: "Drafts", comment: "@context: Post status label" })}
54
- </p>
55
- <p class="text-3xl font-bold">{draftCount}</p>
56
- </div>
57
-
58
- <div class="p-4 border rounded">
59
- <p class="text-sm text-muted-foreground mb-2">
60
- {t({
61
- message: "Quick Actions",
62
- comment: "@context: Dashboard section title",
63
- })}
64
- </p>
65
- <a href="/dash/posts/new" class="btn-primary w-full">
66
- {t({
67
- message: "New Post",
68
- comment: "@context: Button to create new post",
69
- })}
70
- </a>
71
- </div>
72
- </div>
73
-
74
- <p>
75
- <Trans comment="@context: Help text with link">
76
- Need help? Visit the{" "}
77
- <a href="/docs" class="underline">
78
- documentation
79
- </a>
80
- </Trans>
81
- </p>
82
- </>
83
- );
84
- }
85
-
86
- dashIndexRoutes.get("/", async (c) => {
87
- const siteName = c.var.appConfig.siteName;
88
-
89
- // Get stats via service-level counting (avoids loading all posts into memory)
90
- const [publishedCount, draftCount] = await Promise.all([
91
- c.var.services.posts.count({ status: "published" }),
92
- c.var.services.posts.count({ status: "draft" }),
93
- ]);
94
-
95
- return c.html(
96
- <DashLayout c={c} title="Dashboard" siteName={siteName} currentPath="/dash">
97
- <DashboardContent
98
- publishedCount={publishedCount}
99
- draftCount={draftCount}
100
- />
101
- </DashLayout>,
102
- );
103
- });
@@ -1,132 +0,0 @@
1
- /**
2
- * Dashboard Media Routes
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 { DashLayout } from "../../ui/layouts/DashLayout.js";
9
- import { dsRedirect } from "../../lib/sse.js";
10
- import {
11
- getMediaUrl,
12
- getImageUrl,
13
- getPublicUrlForProvider,
14
- } from "../../lib/image.js";
15
- import { MediaListContent } from "../../ui/dash/media/MediaListContent.js";
16
- import { ViewMediaContent } from "../../ui/dash/media/ViewMediaContent.js";
17
-
18
- type Env = { Bindings: Bindings; Variables: AppVariables };
19
-
20
- export const mediaRoutes = new Hono<Env>();
21
-
22
- // List media
23
- mediaRoutes.get("/", async (c) => {
24
- const mediaList = await c.var.services.media.list({ limit: 100 });
25
- const siteName = c.var.appConfig.siteName;
26
-
27
- return c.html(
28
- <DashLayout
29
- c={c}
30
- title="Media"
31
- siteName={siteName}
32
- currentPath="/dash/media"
33
- >
34
- <MediaListContent
35
- mediaList={mediaList}
36
- r2PublicUrl={c.var.appConfig.r2PublicUrl}
37
- imageTransformUrl={c.var.appConfig.imageTransformUrl}
38
- s3PublicUrl={c.var.appConfig.s3PublicUrl}
39
- />
40
- </DashLayout>,
41
- );
42
- });
43
-
44
- // Media picker (returns HTML fragment for PostForm dialog)
45
- // Must be defined before /:id to avoid "picker" matching as an ID
46
- mediaRoutes.get("/picker", async (c) => {
47
- const mediaList = await c.var.services.media.list({
48
- limit: 100,
49
- mimePrefix: "image/",
50
- });
51
- const r2PublicUrl = c.var.appConfig.r2PublicUrl;
52
- const imageTransformUrl = c.var.appConfig.imageTransformUrl;
53
- const s3PublicUrl = c.var.appConfig.s3PublicUrl;
54
-
55
- if (mediaList.length === 0) {
56
- return c.html(
57
- <p class="text-muted-foreground text-sm col-span-4">
58
- No media uploaded yet. Upload media from the Media page first.
59
- </p>,
60
- );
61
- }
62
-
63
- return c.html(
64
- <>
65
- {mediaList.map((m) => {
66
- const pUrl = getPublicUrlForProvider(
67
- m.provider,
68
- r2PublicUrl,
69
- s3PublicUrl,
70
- );
71
- const url = getMediaUrl(m.storageKey, pUrl);
72
- const thumbUrl = getImageUrl(url, imageTransformUrl, {
73
- width: 150,
74
- quality: 80,
75
- format: "auto",
76
- fit: "cover",
77
- });
78
- return (
79
- <button
80
- key={m.id}
81
- type="button"
82
- class="aspect-square rounded-lg overflow-hidden border-2 hover:border-primary cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
83
- data-media-id={m.id}
84
- >
85
- <img
86
- src={thumbUrl}
87
- alt={m.alt || m.originalName}
88
- class="w-full h-full object-cover"
89
- loading="lazy"
90
- />
91
- </button>
92
- );
93
- })}
94
- </>,
95
- );
96
- });
97
-
98
- // View single media
99
- mediaRoutes.get("/:id", async (c) => {
100
- const id = c.req.param("id");
101
- const media = await c.var.services.media.getById(id);
102
- if (!media) return c.notFound();
103
-
104
- const siteName = c.var.appConfig.siteName;
105
-
106
- return c.html(
107
- <DashLayout
108
- c={c}
109
- title={media.originalName}
110
- siteName={siteName}
111
- currentPath="/dash/media"
112
- >
113
- <ViewMediaContent
114
- media={media}
115
- r2PublicUrl={c.var.appConfig.r2PublicUrl}
116
- imageTransformUrl={c.var.appConfig.imageTransformUrl}
117
- s3PublicUrl={c.var.appConfig.s3PublicUrl}
118
- />
119
- </DashLayout>,
120
- );
121
- });
122
-
123
- // Delete media
124
- mediaRoutes.post("/:id/delete", async (c) => {
125
- const id = c.req.param("id");
126
- const media = await c.var.services.media.getById(id);
127
- if (!media) return c.notFound();
128
-
129
- await c.var.services.media.delete(id, c.var.storage);
130
-
131
- return dsRedirect("/dash/media");
132
- });