@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
@@ -14,7 +14,8 @@ import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
14
14
  import { dsRedirect, dsToast } from "../../lib/sse.js";
15
15
  import { SetupSchema } from "../../lib/schemas.js";
16
16
  import { mapIanaToTimezone } from "../../lib/timezones.js";
17
- import { getI18n } from "../../i18n/index.js";
17
+ import { getI18n, baseLocale } from "../../i18n/index.js";
18
+ import { detectLocaleFromHeader } from "../../i18n/detect.js";
18
19
 
19
20
  type Env = { Bindings: Bindings; Variables: AppVariables };
20
21
 
@@ -40,8 +41,8 @@ const SetupContent: FC = () => {
40
41
  </header>
41
42
  <section>
42
43
  <form
43
- data-signals="{name: '', email: '', password: '', _timezone: ''}"
44
- data-init="$_timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || ''"
44
+ data-signals="{siteName: '', email: '', password: '', timezone: '', language: ''}"
45
+ data-init="$timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; $language = navigator.language || ''"
45
46
  data-on:submit__prevent="@post('/setup')"
46
47
  data-indicator="_loading"
47
48
  class="flex flex-col gap-4"
@@ -49,16 +50,16 @@ const SetupContent: FC = () => {
49
50
  <div class="field">
50
51
  <label class="label">
51
52
  {t({
52
- message: "Your Name",
53
- comment: "@context: Setup form field - user name",
53
+ message: "Site Name",
54
+ comment: "@context: Setup form field - site name",
54
55
  })}
55
56
  </label>
56
57
  <input
57
58
  type="text"
58
- data-bind="name"
59
+ data-bind="siteName"
59
60
  class="input"
60
61
  required
61
- placeholder="John Doe"
62
+ placeholder="My Blog"
62
63
  />
63
64
  </div>
64
65
  <div class="field">
@@ -139,27 +140,29 @@ setupRoutes.post("/setup", async (c) => {
139
140
 
140
141
  const body = await c.req.json<Record<string, string>>();
141
142
  const parsed = SetupSchema.safeParse(body);
142
- const browserTimezone = body._timezone;
143
+ const browserTimezone = body.timezone;
144
+ const browserLanguage = body.language;
143
145
 
144
146
  if (!parsed.success) {
145
147
  const errorMsg =
146
148
  parsed.error.issues[0]?.message ??
147
149
  i18n._(
148
150
  msg({
149
- message: "Invalid input",
151
+ message:
152
+ "Something doesn't look right. Check the form and try again.",
150
153
  comment: "@context: Fallback validation error for setup form",
151
154
  }),
152
155
  );
153
156
  return dsToast(errorMsg, "error");
154
157
  }
155
158
 
156
- const { name, email, password } = parsed.data;
159
+ const { siteName, email, password } = parsed.data;
157
160
 
158
161
  if (!c.var.auth) {
159
162
  return dsToast(
160
163
  i18n._(
161
164
  msg({
162
- message: "AUTH_SECRET not configured",
165
+ message: "Auth secret is missing. Check your environment variables.",
163
166
  comment:
164
167
  "@context: Error toast when authentication secret is missing from server config",
165
168
  }),
@@ -170,14 +173,15 @@ setupRoutes.post("/setup", async (c) => {
170
173
 
171
174
  try {
172
175
  const signUpResponse = await c.var.auth.api.signUpEmail({
173
- body: { name, email, password },
176
+ body: { name: siteName.trim(), email, password },
174
177
  });
175
178
 
176
179
  if (!signUpResponse || "error" in signUpResponse) {
177
180
  return dsToast(
178
181
  i18n._(
179
182
  msg({
180
- message: "Failed to create account",
183
+ message:
184
+ "Couldn't create your account. Check the details and try again.",
181
185
  comment: "@context: Error toast when account creation fails",
182
186
  }),
183
187
  ),
@@ -187,6 +191,9 @@ setupRoutes.post("/setup", async (c) => {
187
191
 
188
192
  await c.var.services.settings.completeOnboarding();
189
193
 
194
+ // Save site name
195
+ await c.var.services.settings.set("SITE_NAME", siteName.trim());
196
+
190
197
  // Save auto-detected timezone
191
198
  if (browserTimezone) {
192
199
  const tz = mapIanaToTimezone(browserTimezone);
@@ -195,37 +202,37 @@ setupRoutes.post("/setup", async (c) => {
195
202
  }
196
203
  }
197
204
 
205
+ // Save auto-detected language from browser's navigator.language
206
+ if (browserLanguage) {
207
+ const detectedLang = detectLocaleFromHeader(browserLanguage);
208
+ if (detectedLang !== baseLocale) {
209
+ await c.var.services.settings.set("SITE_LANGUAGE", detectedLang);
210
+ }
211
+ }
212
+
213
+ // Seed default navigation items (order: Collections, Archive, RSS, Settings)
198
214
  await c.var.services.navItems.create({
199
215
  type: "link",
200
216
  label: "Collections",
201
217
  url: "/c",
202
218
  });
203
- // Seed default navigation items
219
+
204
220
  await c.var.services.navItems.create({
205
221
  type: "link",
206
222
  label: "Archive",
207
223
  url: "/archive",
208
224
  });
209
225
 
210
- // Seed default About page
211
- const aboutPage = await c.var.services.pages.create({
212
- slug: "about",
213
- title: "About",
214
- body: [
215
- "Welcome to my corner of the internet.",
216
- "",
217
- "This is a place where I share my thoughts, ideas, and things I find interesting. Feel free to look around and get to know what this site is all about.",
218
- "",
219
- "If you'd like to get in touch, don't hesitate to reach out.",
220
- ].join("\n"),
221
- status: "published",
226
+ await c.var.services.navItems.create({
227
+ type: "system",
228
+ label: "RSS",
229
+ url: "/feed",
222
230
  });
223
231
 
224
232
  await c.var.services.navItems.create({
225
- type: "page",
226
- label: "About",
227
- url: "/about",
228
- pageId: aboutPage.id,
233
+ type: "system",
234
+ label: "Settings",
235
+ url: "/settings",
229
236
  });
230
237
 
231
238
  return dsRedirect("/signin?setup");
@@ -235,7 +242,8 @@ setupRoutes.post("/setup", async (c) => {
235
242
  return dsToast(
236
243
  i18n._(
237
244
  msg({
238
- message: "Failed to create account",
245
+ message:
246
+ "Couldn't create your account. Check the details and try again.",
239
247
  comment: "@context: Error toast when account creation fails",
240
248
  }),
241
249
  ),
@@ -40,7 +40,8 @@ const SigninContent: FC<{
40
40
  {demoEmail && demoPassword && (
41
41
  <p class="text-muted-foreground text-sm mb-4">
42
42
  {t({
43
- message: "Demo account pre-filled. Just click Sign In.",
43
+ message:
44
+ "Demo credentials are pre-filled — hit Sign In to continue.",
44
45
  comment:
45
46
  "@context: Hint shown on signin page when demo credentials are pre-filled",
46
47
  })}
@@ -110,9 +111,9 @@ signinRoutes.get("/signin", async (c) => {
110
111
  const isReset = c.req.query("reset") !== undefined;
111
112
  let toast: { message: string } | undefined;
112
113
  if (isSetup) {
113
- toast = { message: "Account created successfully. Please sign in." };
114
+ toast = { message: "Account created. Sign in to get started." };
114
115
  } else if (isReset) {
115
- toast = { message: "Password reset successfully. Please sign in." };
116
+ toast = { message: "Password reset. Sign in with your new password." };
116
117
  }
117
118
 
118
119
  return c.html(
@@ -132,7 +133,7 @@ signinRoutes.post("/signin", async (c) => {
132
133
  return dsToast(
133
134
  i18n._(
134
135
  msg({
135
- message: "Auth not configured",
136
+ message: "Authentication isn't set up. Check your server config.",
136
137
  comment:
137
138
  "@context: Error toast when authentication system is unavailable",
138
139
  }),
@@ -149,7 +150,8 @@ signinRoutes.post("/signin", async (c) => {
149
150
  parsed.error.issues[0]?.message ??
150
151
  i18n._(
151
152
  msg({
152
- message: "Invalid input",
153
+ message:
154
+ "Something doesn't look right. Check the form and try again.",
153
155
  comment: "@context: Fallback validation error for sign-in form",
154
156
  }),
155
157
  );
@@ -165,12 +167,13 @@ signinRoutes.post("/signin", async (c) => {
165
167
  headers: c.req.raw.headers,
166
168
  });
167
169
 
168
- return dsRedirect("/dash", { headers });
170
+ return dsRedirect("/", { headers });
169
171
  } catch {
170
172
  return dsToast(
171
173
  i18n._(
172
174
  msg({
173
- message: "Invalid email or password",
175
+ message:
176
+ "Wrong email or password. Check your credentials and try again.",
174
177
  comment: "@context: Error toast when sign-in credentials are wrong",
175
178
  }),
176
179
  ),
@@ -179,21 +182,17 @@ signinRoutes.post("/signin", async (c) => {
179
182
  }
180
183
  });
181
184
 
182
- signinRoutes.get("/signout", async (c) => {
185
+ signinRoutes.post("/signout", async (c) => {
183
186
  if (c.var.auth) {
184
187
  try {
185
188
  const res = await c.var.auth.api.signOut({
186
189
  headers: c.req.raw.headers,
187
190
  asResponse: true,
188
191
  });
189
- const redirect = c.redirect("/");
190
- for (const cookie of res.headers.getSetCookie()) {
191
- redirect.headers.append("Set-Cookie", cookie);
192
- }
193
- return redirect;
192
+ return dsRedirect("/", { headers: res.headers });
194
193
  } catch {
195
194
  // Ignore signout errors
196
195
  }
197
196
  }
198
- return c.redirect("/");
197
+ return dsRedirect("/");
199
198
  });
@@ -2,26 +2,19 @@
2
2
  * Compose Route
3
3
  *
4
4
  * Handles post creation from the public-site compose dialog.
5
- * Published posts are prepended to the homepage timeline via SSE.
5
+ * On publish the client reloads the page to pick up the new post.
6
6
  * Drafts close the dialog and show a confirmation toast.
7
7
  */
8
8
 
9
- import { Hono, type Context } from "hono";
9
+ import { Hono } from "hono";
10
10
  import { msg } from "@lingui/core/macro";
11
- import type { Bindings, Post } from "../types.js";
11
+ import type { Bindings } from "../types.js";
12
12
  import type { AppVariables } from "../types/app-context.js";
13
13
  import { requireAuth } from "../middleware/auth.js";
14
14
  import { CreatePostSchema } from "../lib/schemas.js";
15
15
  import { ValidationError } from "../lib/errors.js";
16
16
  import { sse, dsToast } from "../lib/sse.js";
17
17
  import { getI18n } from "../i18n/index.js";
18
- import {
19
- toPostView,
20
- toPostViewFromPost,
21
- createMediaContext,
22
- } from "../lib/view.js";
23
- import { buildMediaMap } from "../lib/media-helpers.js";
24
- import { TimelineItemFromPost } from "../ui/feed/TimelineItem.js";
25
18
 
26
19
  type Env = { Bindings: Bindings; Variables: AppVariables };
27
20
 
@@ -48,40 +41,7 @@ const INITIAL_SIGNALS = {
48
41
 
49
42
  /** Script fragment that closes the compose dialog and self-removes */
50
43
  const CLOSE_DIALOG_SCRIPT =
51
- "<script data-effect=\"el.remove()\">document.getElementById('compose-dialog').close()</script>";
52
-
53
- /** Build a timeline card HTML string for a newly created post */
54
- async function buildTimelineCard(
55
- c: Context<Env>,
56
- post: Post,
57
- mediaIds: string[] | undefined,
58
- ): Promise<string> {
59
- const mediaCtx = createMediaContext(c.var.appConfig);
60
- let postView;
61
-
62
- if (mediaIds && mediaIds.length > 0) {
63
- const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
64
- const mediaMap = buildMediaMap(
65
- rawMediaMap,
66
- mediaCtx.r2PublicUrl,
67
- mediaCtx.imageTransformUrl,
68
- mediaCtx.s3PublicUrl,
69
- );
70
- postView = toPostView(
71
- { ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
72
- mediaCtx,
73
- );
74
- } else {
75
- postView = toPostViewFromPost(post, mediaCtx);
76
- }
77
-
78
- return (
79
- <div>
80
- <TimelineItemFromPost post={postView} />
81
- <hr class="feed-divider" />
82
- </div>
83
- ).toString();
84
- }
44
+ "<div data-init=\"document.getElementById('compose-dialog').close(); el.remove()\"></div>";
85
45
 
86
46
  composeRoutes.post("/", async (c) => {
87
47
  const i18n = getI18n(c);
@@ -94,7 +54,8 @@ composeRoutes.post("/", async (c) => {
94
54
  result.error.issues[0]?.message ??
95
55
  i18n._(
96
56
  msg({
97
- message: "Invalid input",
57
+ message:
58
+ "Something doesn't look right. Check the form and try again.",
98
59
  comment: "@context: Fallback validation error for compose form",
99
60
  }),
100
61
  );
@@ -121,16 +82,26 @@ composeRoutes.post("/", async (c) => {
121
82
  }
122
83
  }
123
84
 
124
- const post = await c.var.services.posts.create({
125
- format: data.format,
126
- title: data.title || undefined,
127
- body: data.body || undefined,
128
- status: data.status ?? "published",
129
- url: data.url || undefined,
130
- quoteText: data.quoteText || undefined,
131
- rating: data.rating || undefined,
132
- collectionIds: data.collectionIds?.length ? data.collectionIds : undefined,
133
- });
85
+ const post = await c.var.services.posts.create(
86
+ {
87
+ format: data.format,
88
+ title: data.title || undefined,
89
+ body: data.body || undefined,
90
+ bodyMarkdown: data.bodyMarkdown || undefined,
91
+ status: data.status ?? "published",
92
+ visibility: data.visibility || undefined,
93
+ featured: data.featured,
94
+ url: data.url || undefined,
95
+ quoteText: data.quoteText || undefined,
96
+ rating: data.rating || undefined,
97
+ collectionIds: data.collectionIds,
98
+ replyToId: data.replyToId,
99
+ },
100
+ {
101
+ maxParagraphs: c.var.appConfig.summaryMaxParagraphs,
102
+ maxChars: c.var.appConfig.summaryMaxChars,
103
+ },
104
+ );
134
105
 
135
106
  // Attach media if provided
136
107
  if (data.mediaIds && data.mediaIds.length > 0) {
@@ -163,8 +134,7 @@ composeRoutes.post("/", async (c) => {
163
134
  });
164
135
  }
165
136
 
166
- const cardHtml = await buildTimelineCard(c, post, data.mediaIds);
167
- return c.json({ status: "published" as const, cardHtml });
137
+ return c.json({ status: "published" as const, permalink: `/${post.slug}` });
168
138
  }
169
139
 
170
140
  // ── SSE response mode (used by Datastar) ─────────────────────────
@@ -186,13 +156,7 @@ composeRoutes.post("/", async (c) => {
186
156
  });
187
157
  }
188
158
 
189
- const cardHtml = await buildTimelineCard(c, post, data.mediaIds);
190
-
191
159
  return sse(c, async (stream) => {
192
- await stream.patchElements(cardHtml, {
193
- mode: "prepend",
194
- selector: "#timeline-items",
195
- });
196
160
  await stream.patchElements(CLOSE_DIALOG_SCRIPT, {
197
161
  mode: "append",
198
162
  selector: "body",
@@ -38,7 +38,7 @@ function createMockFile(
38
38
  };
39
39
  }
40
40
 
41
- describe("Dashboard Settings - Avatar Upload Logic", () => {
41
+ describe("Settings - Avatar Upload Logic", () => {
42
42
  let db: Database;
43
43
  let settingsService: ReturnType<typeof createSettingsService>;
44
44
  let mediaService: ReturnType<typeof createMediaService>;
@@ -57,7 +57,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
57
57
 
58
58
  await settingsService.uploadAvatar(
59
59
  { file },
60
- { media: mediaService, storage, storageProvider: "r2" },
60
+ {
61
+ media: mediaService,
62
+ storage,
63
+ storageProvider: "r2",
64
+ maxFileSizeMB: 500,
65
+ },
61
66
  );
62
67
 
63
68
  const avatarKey = await settingsService.get("SITE_AVATAR");
@@ -72,7 +77,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
72
77
 
73
78
  await settingsService.uploadAvatar(
74
79
  { file },
75
- { media: mediaService, storage, storageProvider: "r2" },
80
+ {
81
+ media: mediaService,
82
+ storage,
83
+ storageProvider: "r2",
84
+ maxFileSizeMB: 500,
85
+ },
76
86
  );
77
87
 
78
88
  const mediaList = await mediaService.list();
@@ -89,7 +99,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
89
99
 
90
100
  await settingsService.uploadAvatar(
91
101
  { file, faviconIco: fakeIcoData.buffer },
92
- { media: mediaService, storage, storageProvider: "r2" },
102
+ {
103
+ media: mediaService,
104
+ storage,
105
+ storageProvider: "r2",
106
+ maxFileSizeMB: 500,
107
+ },
93
108
  );
94
109
 
95
110
  const stored = await settingsService.get("SITE_FAVICON_ICO");
@@ -105,7 +120,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
105
120
 
106
121
  await settingsService.uploadAvatar(
107
122
  { file, appleTouchIcon: appleTouchData },
108
- { media: mediaService, storage, storageProvider: "r2" },
123
+ {
124
+ media: mediaService,
125
+ storage,
126
+ storageProvider: "r2",
127
+ maxFileSizeMB: 500,
128
+ },
109
129
  );
110
130
 
111
131
  const stored = await settingsService.get("SITE_FAVICON_APPLE_TOUCH");
@@ -120,7 +140,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
120
140
 
121
141
  await settingsService.uploadAvatar(
122
142
  { file },
123
- { media: mediaService, storage, storageProvider: "r2" },
143
+ {
144
+ media: mediaService,
145
+ storage,
146
+ storageProvider: "r2",
147
+ maxFileSizeMB: 500,
148
+ },
124
149
  );
125
150
 
126
151
  const stored = await settingsService.get("SITE_FAVICON_VERSION");
@@ -135,19 +160,29 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
135
160
  await expect(
136
161
  settingsService.uploadAvatar(
137
162
  { file },
138
- { media: mediaService, storage, storageProvider: "r2" },
163
+ {
164
+ media: mediaService,
165
+ storage,
166
+ storageProvider: "r2",
167
+ maxFileSizeMB: 500,
168
+ },
139
169
  ),
140
170
  ).rejects.toThrow("File type not allowed");
141
171
  });
142
172
 
143
173
  it("throws ValidationError for oversized file", async () => {
144
174
  const storage = createMockStorage();
145
- const file = createMockFile("big.png", "image/png", 20 * 1024 * 1024);
175
+ const file = createMockFile("big.png", "image/png", 501 * 1024 * 1024);
146
176
 
147
177
  await expect(
148
178
  settingsService.uploadAvatar(
149
179
  { file },
150
- { media: mediaService, storage, storageProvider: "r2" },
180
+ {
181
+ media: mediaService,
182
+ storage,
183
+ storageProvider: "r2",
184
+ maxFileSizeMB: 500,
185
+ },
151
186
  ),
152
187
  ).rejects.toThrow("File too large");
153
188
  });