@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.
- package/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -140
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +57 -31
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /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
|
-
});
|
package/src/lib/media-upload.ts
DELETED
|
@@ -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
|
-
}
|