@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,65 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { encode, decode, isValidSqid } from "../sqid.js";
3
-
4
- describe("encode", () => {
5
- it("encodes a numeric ID to a string", () => {
6
- const result = encode(1);
7
- expect(typeof result).toBe("string");
8
- expect(result.length).toBeGreaterThanOrEqual(4);
9
- });
10
-
11
- it("produces minimum 4-character strings", () => {
12
- expect(encode(0).length).toBeGreaterThanOrEqual(4);
13
- expect(encode(1).length).toBeGreaterThanOrEqual(4);
14
- expect(encode(100).length).toBeGreaterThanOrEqual(4);
15
- });
16
-
17
- it("produces different strings for different IDs", () => {
18
- const a = encode(1);
19
- const b = encode(2);
20
- const c = encode(100);
21
- expect(a).not.toBe(b);
22
- expect(b).not.toBe(c);
23
- });
24
-
25
- it("produces consistent results for the same ID", () => {
26
- expect(encode(42)).toBe(encode(42));
27
- });
28
- });
29
-
30
- describe("decode", () => {
31
- it("decodes an encoded string back to the original ID", () => {
32
- for (const id of [0, 1, 42, 100, 999, 10000]) {
33
- const encoded = encode(id);
34
- expect(decode(encoded)).toBe(id);
35
- }
36
- });
37
-
38
- it("returns null for empty string", () => {
39
- expect(decode("")).toBe(null);
40
- });
41
-
42
- it("handles round-trip encoding", () => {
43
- const original = 12345;
44
- const sqid = encode(original);
45
- const decoded = decode(sqid);
46
- expect(decoded).toBe(original);
47
- });
48
- });
49
-
50
- describe("isValidSqid", () => {
51
- it("returns true for valid encoded sqids", () => {
52
- const sqid = encode(1);
53
- expect(isValidSqid(sqid)).toBe(true);
54
- });
55
-
56
- it("returns true for various valid sqids", () => {
57
- for (const id of [0, 1, 100, 999]) {
58
- expect(isValidSqid(encode(id))).toBe(true);
59
- }
60
- });
61
-
62
- it("returns false for empty string", () => {
63
- expect(isValidSqid("")).toBe(false);
64
- });
65
- });
@@ -1,28 +0,0 @@
1
- /**
2
- * Collection Reorder
3
- *
4
- * Initializes SortableJS on the collections list in the dashboard.
5
- * Auto-detects the list element and only activates when present.
6
- * Sends prefixed string IDs (e.g. "c-1", "d-2") to support mixed
7
- * collections and dividers in a unified sort order.
8
- */
9
-
10
- import Sortable from "sortablejs";
11
-
12
- const list = document.getElementById("collections-list");
13
- if (list) {
14
- Sortable.create(list, {
15
- animation: 150,
16
- handle: "[data-id]",
17
- onEnd() {
18
- const items = [...list.querySelectorAll<HTMLElement>("[data-id]")]
19
- .map((el) => el.dataset.id)
20
- .filter((id): id is string => id !== undefined);
21
- fetch("/dash/collections/reorder", {
22
- method: "POST",
23
- headers: { "Content-Type": "application/json" },
24
- body: JSON.stringify({ items }),
25
- });
26
- },
27
- });
28
- }
@@ -1,280 +0,0 @@
1
- /**
2
- * Compose Bridge
3
- *
4
- * Handles server communication between the Lit compose dialog and the server.
5
- * Manages file uploads, deferred submit flow, and toast notifications.
6
- */
7
-
8
- import type { ComposeSubmitDetail } from "../ui/components/compose-types.js";
9
- import type { ComposeAttachment } from "../ui/components/compose-types.js";
10
- import type { JantComposeDialog } from "../ui/components/jant-compose-dialog.js";
11
- import type { JantComposeEditor } from "../ui/components/jant-compose-editor.js";
12
- import { ImageProcessor } from "./image-processor.js";
13
- import {
14
- showToast,
15
- showPersistentToast,
16
- replaceWithAutoClose,
17
- } from "./toast.js";
18
-
19
- // ── Upload manager ──────────────────────────────────────────────────
20
-
21
- /** Track in-flight upload promises keyed by clientId */
22
- const uploadPromises = new Map<string, Promise<string | null>>();
23
-
24
- /** Track attachments removed while their upload is still in flight */
25
- const removedClientIds = new Set<string>();
26
-
27
- /**
28
- * Upload a single file: process with ImageProcessor, then POST to /api/upload.
29
- * Returns the mediaId on success, null on failure.
30
- */
31
- async function uploadFile(
32
- file: File,
33
- clientId: string,
34
- editor: JantComposeEditor | null,
35
- ): Promise<string | null> {
36
- try {
37
- // Update status to uploading
38
- editor?.updateAttachmentStatus(clientId, "uploading", null, null);
39
-
40
- // Process image (resize, convert to WebP)
41
- const processed = await ImageProcessor.processToFile(file);
42
-
43
- // Upload to server
44
- const formData = new FormData();
45
- formData.append("file", processed);
46
-
47
- const res = await fetch("/api/upload", {
48
- method: "POST",
49
- body: formData,
50
- });
51
-
52
- if (!res.ok) {
53
- const data = await res.json();
54
- const error = data.error ?? "Upload failed";
55
- editor?.updateAttachmentStatus(clientId, "error", null, error);
56
- return null;
57
- }
58
-
59
- const data = await res.json();
60
- const mediaId = data.id as string;
61
- editor?.updateAttachmentStatus(clientId, "done", mediaId, null);
62
- return mediaId;
63
- } catch {
64
- editor?.updateAttachmentStatus(clientId, "error", null, "Upload failed");
65
- return null;
66
- }
67
- }
68
-
69
- function getEditor(): JantComposeEditor | null {
70
- return document.querySelector("jant-compose-editor");
71
- }
72
-
73
- // ── Attachment removal handler ───────────────────────────────────────
74
-
75
- document.addEventListener("jant:attachment-removed", (e: Event) => {
76
- const { clientId, mediaId } = (
77
- e as CustomEvent<{ clientId: string; mediaId: string | null }>
78
- ).detail;
79
-
80
- if (mediaId) {
81
- // Upload already finished — fire-and-forget delete
82
- fetch(`/api/upload/${mediaId}`, { method: "DELETE" }).catch(() => {});
83
- } else {
84
- // Upload still in flight — mark for cleanup after it finishes
85
- removedClientIds.add(clientId);
86
- }
87
- });
88
-
89
- // ── File selection handler ──────────────────────────────────────────
90
-
91
- document.addEventListener("jant:files-selected", (e: Event) => {
92
- const event = e as CustomEvent<{
93
- files: { file: File; clientId: string }[];
94
- }>;
95
- const editor = getEditor();
96
-
97
- for (const { file, clientId } of event.detail.files) {
98
- const promise = uploadFile(file, clientId, editor).then((mediaId) => {
99
- // If the attachment was removed while uploading, delete it immediately
100
- if (removedClientIds.has(clientId)) {
101
- removedClientIds.delete(clientId);
102
- if (mediaId) {
103
- fetch(`/api/upload/${mediaId}`, { method: "DELETE" }).catch(() => {});
104
- }
105
- return null;
106
- }
107
- return mediaId;
108
- });
109
- uploadPromises.set(clientId, promise);
110
- promise.finally(() => uploadPromises.delete(clientId));
111
- }
112
- });
113
-
114
- // ── Submit handler ──────────────────────────────────────────────────
115
-
116
- document.addEventListener("jant:compose-submit", async (e: Event) => {
117
- const event = e as CustomEvent<ComposeSubmitDetail>;
118
- const detail = event.detail;
119
- const dialog = document.getElementById(
120
- "compose-dialog",
121
- ) as HTMLDialogElement | null;
122
- const composeEl = document.querySelector(
123
- "jant-compose-dialog",
124
- ) as JantComposeDialog | null;
125
-
126
- if (!composeEl) return;
127
- composeEl.loading = true;
128
-
129
- try {
130
- const res = await fetch("/compose", {
131
- method: "POST",
132
- headers: {
133
- "Content-Type": "application/json",
134
- Accept: "application/json",
135
- },
136
- body: JSON.stringify({
137
- format: detail.format,
138
- title: detail.title || undefined,
139
- body: detail.body || undefined,
140
- url: detail.url || undefined,
141
- quoteText: detail.quoteText || undefined,
142
- status: detail.status,
143
- rating: detail.rating || undefined,
144
- collectionIds:
145
- detail.collectionIds.length > 0 ? detail.collectionIds : undefined,
146
- mediaIds: detail.mediaIds.length > 0 ? detail.mediaIds : undefined,
147
- mediaAlts:
148
- Object.keys(detail.mediaAlts).length > 0
149
- ? detail.mediaAlts
150
- : undefined,
151
- }),
152
- });
153
-
154
- if (!res.ok) {
155
- const data = await res.json();
156
- showToast(data.error ?? "Something went wrong", "error");
157
- return;
158
- }
159
-
160
- const data = await res.json();
161
-
162
- if (data.status === "draft") {
163
- showToast(data.toast ?? "Draft saved.");
164
- } else if (data.status === "published" && data.cardHtml) {
165
- const timeline = document.getElementById("timeline-items");
166
- if (timeline) {
167
- document.getElementById("empty-timeline")?.remove();
168
- timeline.insertAdjacentHTML("afterbegin", data.cardHtml);
169
- }
170
- }
171
-
172
- dialog?.close();
173
- // Prevent browser from restoring focus to the trigger button
174
- (document.activeElement as HTMLElement)?.blur();
175
- composeEl.reset();
176
- } catch {
177
- showToast("Something went wrong", "error");
178
- } finally {
179
- composeEl.loading = false;
180
- }
181
- });
182
-
183
- // ── Deferred submit handler ─────────────────────────────────────────
184
-
185
- interface DeferredDetail extends ComposeSubmitDetail {
186
- pendingAttachments: ComposeAttachment[];
187
- }
188
-
189
- document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
190
- const event = e as CustomEvent<DeferredDetail>;
191
- const detail = event.detail;
192
- const composeEl = document.querySelector(
193
- "jant-compose-dialog",
194
- ) as JantComposeDialog | null;
195
-
196
- // Get labels for toast messages
197
- const labels = composeEl?.labels;
198
- const uploadingMsg = labels?.uploading ?? "Uploading...";
199
- const publishedMsg = labels?.published ?? "Published!";
200
-
201
- // Show persistent toast
202
- showPersistentToast("compose-deferred", uploadingMsg);
203
-
204
- try {
205
- // Wait for all pending uploads to complete
206
- const pendingClientIds = detail.pendingAttachments.map((a) => a.clientId);
207
- const pendingPromises = pendingClientIds
208
- .map((id) => uploadPromises.get(id))
209
- .filter((p): p is Promise<string | null> => p !== undefined);
210
-
211
- const results = await Promise.all(pendingPromises);
212
-
213
- // Merge newly completed mediaIds with already-done ones
214
- const newMediaIds = results.filter((id): id is string => id !== null);
215
- const allMediaIds = [...detail.mediaIds, ...newMediaIds];
216
-
217
- // Merge alt text: for pending attachments that just uploaded,
218
- // map their clientId → mediaId and include their alt text
219
- const mediaAlts = { ...detail.mediaAlts };
220
- for (const att of detail.pendingAttachments) {
221
- if (att.alt) {
222
- // Find the mediaId from the upload result by matching clientId position
223
- const idx = pendingClientIds.indexOf(att.clientId);
224
- const mediaId = results[idx];
225
- if (mediaId) {
226
- mediaAlts[mediaId] = att.alt;
227
- }
228
- }
229
- }
230
-
231
- // POST to /compose
232
- const res = await fetch("/compose", {
233
- method: "POST",
234
- headers: {
235
- "Content-Type": "application/json",
236
- Accept: "application/json",
237
- },
238
- body: JSON.stringify({
239
- format: detail.format,
240
- title: detail.title || undefined,
241
- body: detail.body || undefined,
242
- url: detail.url || undefined,
243
- quoteText: detail.quoteText || undefined,
244
- status: detail.status,
245
- rating: detail.rating || undefined,
246
- collectionIds:
247
- detail.collectionIds.length > 0 ? detail.collectionIds : undefined,
248
- mediaIds: allMediaIds.length > 0 ? allMediaIds : undefined,
249
- mediaAlts: Object.keys(mediaAlts).length > 0 ? mediaAlts : undefined,
250
- }),
251
- });
252
-
253
- if (!res.ok) {
254
- const data = await res.json();
255
- replaceWithAutoClose(
256
- "compose-deferred",
257
- data.error ?? "Something went wrong",
258
- "error",
259
- );
260
- return;
261
- }
262
-
263
- const data = await res.json();
264
-
265
- if (data.status === "published" && data.cardHtml) {
266
- const timeline = document.getElementById("timeline-items");
267
- if (timeline) {
268
- document.getElementById("empty-timeline")?.remove();
269
- timeline.insertAdjacentHTML("afterbegin", data.cardHtml);
270
- }
271
- }
272
-
273
- replaceWithAutoClose(
274
- "compose-deferred",
275
- data.status === "draft" ? (data.toast ?? "Draft saved.") : publishedMsg,
276
- );
277
- } catch {
278
- replaceWithAutoClose("compose-deferred", "Something went wrong", "error");
279
- }
280
- });
@@ -1,148 +0,0 @@
1
- /**
2
- * Client-side Media Upload Handler
3
- *
4
- * Handles file upload flow:
5
- * 1. User selects file via [data-media-upload] input
6
- * 2. Creates placeholder in grid with spinner
7
- * 3. Processes image via ImageProcessor (resize/convert to WebP)
8
- * 4. Sets processed file on hidden Datastar form via DataTransfer API
9
- * 5. Triggers form.requestSubmit() — Datastar handles upload + SSE response
10
- */
11
-
12
- import { ImageProcessor } from "./image-processor.js";
13
-
14
- /**
15
- * Format file size for display
16
- */
17
- function formatFileSize(bytes: number): string {
18
- if (bytes < 1024) return `${bytes} B`;
19
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
20
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
21
- }
22
-
23
- /**
24
- * Ensure the media grid exists, removing empty state if needed
25
- */
26
- function ensureGridExists(): HTMLElement {
27
- let grid = document.getElementById("media-grid");
28
- if (grid) return grid;
29
-
30
- document.getElementById("empty-state")?.remove();
31
-
32
- grid = document.createElement("div");
33
- grid.id = "media-grid";
34
- grid.className =
35
- "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4";
36
- document.getElementById("media-content")?.appendChild(grid);
37
- return grid;
38
- }
39
-
40
- /**
41
- * Create a placeholder card with spinner in the media grid
42
- */
43
- function createPlaceholder(
44
- fileName: string,
45
- fileSize: number,
46
- statusText: string,
47
- ): HTMLElement {
48
- const placeholder = document.createElement("div");
49
- placeholder.id = "upload-placeholder";
50
- placeholder.className = "group relative";
51
- placeholder.innerHTML = `
52
- <div class="aspect-square bg-muted rounded-lg overflow-hidden border flex items-center justify-center">
53
- <div class="text-center px-2">
54
- <svg class="animate-spin h-6 w-6 text-muted-foreground mx-auto mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
55
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
56
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
57
- </svg>
58
- <span id="upload-status" class="text-xs text-muted-foreground">${statusText}</span>
59
- </div>
60
- </div>
61
- <div class="mt-2 text-xs truncate" title="${fileName}">${fileName}</div>
62
- <div class="text-xs text-muted-foreground">${formatFileSize(fileSize)}</div>
63
- `;
64
- return placeholder;
65
- }
66
-
67
- /**
68
- * Replace placeholder content with an error message
69
- */
70
- function showPlaceholderError(
71
- placeholder: HTMLElement,
72
- fileName: string,
73
- errorMessage: string,
74
- ): void {
75
- placeholder.innerHTML = `
76
- <div class="aspect-square bg-destructive/10 rounded-lg overflow-hidden border border-destructive flex items-center justify-center">
77
- <div class="text-center px-2">
78
- <span class="text-xs text-destructive">${errorMessage}</span>
79
- </div>
80
- </div>
81
- <div class="mt-2 text-xs truncate text-destructive">${fileName}</div>
82
- <button type="button" class="text-xs text-muted-foreground hover:underline" onclick="this.closest('.group').remove()">Remove</button>
83
- `;
84
- }
85
-
86
- /**
87
- * Handle the upload flow for a selected file
88
- */
89
- async function handleUpload(
90
- input: HTMLInputElement,
91
- file: File,
92
- ): Promise<void> {
93
- const processingText = input.dataset.textProcessing || "Processing...";
94
- const uploadingText = input.dataset.textUploading || "Uploading...";
95
- const errorText =
96
- input.dataset.textError || "Upload failed. Please try again.";
97
-
98
- const grid = ensureGridExists();
99
- const placeholder = createPlaceholder(file.name, file.size, processingText);
100
- grid.prepend(placeholder);
101
-
102
- try {
103
- // Process image client-side (resize, convert to WebP)
104
- const processed = await ImageProcessor.processToFile(file);
105
-
106
- // Update status
107
- const statusEl = document.getElementById("upload-status");
108
- if (statusEl) statusEl.textContent = uploadingText;
109
-
110
- // Set processed file on hidden form input via DataTransfer API
111
- const formInput = document.getElementById(
112
- "upload-file-input",
113
- ) as HTMLInputElement | null;
114
- const form = document.getElementById(
115
- "upload-form",
116
- ) as HTMLFormElement | null;
117
- if (!formInput || !form) throw new Error("Upload form not found");
118
-
119
- const dt = new DataTransfer();
120
- dt.items.add(processed);
121
- formInput.files = dt.files;
122
-
123
- // Trigger Datastar-intercepted form submission
124
- form.requestSubmit();
125
- } catch (err) {
126
- const message = err instanceof Error ? err.message : errorText;
127
- showPlaceholderError(placeholder, file.name, message);
128
- }
129
-
130
- // Reset file input so the same file can be re-selected
131
- input.value = "";
132
- }
133
-
134
- /**
135
- * Initialize media upload via event delegation
136
- */
137
- function initMediaUpload(): void {
138
- document.addEventListener("change", (e) => {
139
- const input = (e.target as HTMLElement).closest(
140
- "[data-media-upload]",
141
- ) as HTMLInputElement | null;
142
- if (!input?.files?.[0]) return;
143
-
144
- handleUpload(input, input.files[0]);
145
- });
146
- }
147
-
148
- initMediaUpload();
package/src/lib/sqid.ts DELETED
@@ -1,79 +0,0 @@
1
- /**
2
- * Sqids - Short unique IDs for URLs
3
- *
4
- * Encodes numeric IDs to short strings like "jR3k"
5
- */
6
-
7
- import Sqids from "sqids";
8
-
9
- const sqids = new Sqids({
10
- minLength: 4,
11
- });
12
-
13
- /**
14
- * Encodes a numeric database ID to a short, URL-friendly string.
15
- *
16
- * Uses the Sqids library to generate short unique identifiers with a minimum length of 4 characters.
17
- * These are used in URLs (e.g., `/p/jR3k`) to obscure sequential integer IDs while maintaining
18
- * uniqueness and reversibility.
19
- *
20
- * @param id - The numeric database ID to encode
21
- * @returns A short string representation of the ID (minimum 4 characters)
22
- *
23
- * @example
24
- * ```ts
25
- * const sqid = encode(123);
26
- * // Returns: "jR3k" (or similar short string)
27
- * ```
28
- */
29
- export function encode(id: number): string {
30
- return sqids.encode([id]);
31
- }
32
-
33
- /**
34
- * Decodes a sqid string back to the original numeric database ID.
35
- *
36
- * Attempts to decode a sqid string generated by the `encode()` function. Returns the original
37
- * numeric ID if valid, or `null` if the string is not a valid sqid. This is used to extract
38
- * database IDs from URL parameters.
39
- *
40
- * @param str - The sqid string to decode
41
- * @returns The original numeric ID if valid, or `null` if decoding fails
42
- *
43
- * @example
44
- * ```ts
45
- * const id = decode("jR3k");
46
- * // Returns: 123
47
- *
48
- * const invalid = decode("invalid");
49
- * // Returns: null
50
- * ```
51
- */
52
- export function decode(str: string): number | null {
53
- try {
54
- const ids = sqids.decode(str);
55
- return ids[0] ?? null;
56
- } catch {
57
- return null;
58
- }
59
- }
60
-
61
- /**
62
- * Checks if a string is a valid sqid that can be decoded.
63
- *
64
- * Validates whether a string can be successfully decoded to a numeric ID.
65
- * Useful for route validation and input sanitization.
66
- *
67
- * @param str - The string to validate
68
- * @returns `true` if the string is a valid sqid, `false` otherwise
69
- *
70
- * @example
71
- * ```ts
72
- * if (isValidSqid("jR3k")) {
73
- * // Process the valid sqid
74
- * }
75
- * ```
76
- */
77
- export function isValidSqid(str: string): boolean {
78
- return decode(str) !== null;
79
- }