@jant/core 0.3.26 → 0.3.28
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/dist/client/client.css +1 -0
- package/dist/client/client.js +31561 -0
- package/dist/index.js +15209 -15
- package/package.json +21 -15
- package/src/__tests__/helpers/app.ts +19 -3
- package/src/__tests__/helpers/db.ts +44 -0
- package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
- package/src/app.tsx +112 -173
- package/src/auth.ts +4 -1
- package/src/client.ts +13 -0
- package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
- package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
- package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
- package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
- package/src/db/schema.ts +24 -4
- package/src/i18n/locales/en.po +810 -385
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +733 -522
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +733 -522
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +7 -11
- package/src/index.ts +1 -1
- package/src/lib/__tests__/icons.test.ts +178 -0
- package/src/lib/__tests__/resolve-config.test.ts +184 -0
- package/src/lib/__tests__/schemas.test.ts +12 -6
- package/src/lib/__tests__/theme.test.ts +62 -0
- package/src/lib/__tests__/timezones.test.ts +1 -1
- package/src/lib/__tests__/url.test.ts +12 -0
- package/src/lib/__tests__/view.test.ts +1 -5
- package/src/lib/avatar-upload.ts +18 -10
- package/src/lib/collection-form-bridge.ts +52 -0
- package/src/lib/collections-reorder.ts +28 -0
- package/src/lib/compose-bridge.ts +251 -0
- package/src/lib/errors.ts +116 -0
- package/src/lib/excerpt.ts +1 -1
- package/src/lib/favicon.ts +3 -5
- package/src/lib/html.ts +22 -0
- package/src/lib/icon-catalog.ts +181 -0
- package/src/lib/icons.ts +202 -0
- package/src/lib/navigation.ts +18 -33
- package/src/lib/pagination.ts +3 -2
- package/src/lib/post-form-bridge.ts +136 -0
- package/src/lib/render.tsx +11 -4
- package/src/lib/resolve-config.ts +157 -0
- package/src/lib/schemas.ts +76 -12
- package/src/lib/settings-bridge.ts +139 -0
- package/src/lib/storage.ts +37 -16
- package/src/lib/theme.ts +5 -7
- package/src/lib/timeline.ts +4 -8
- package/src/lib/toast.ts +134 -0
- package/src/lib/upload.ts +71 -0
- package/src/lib/url.ts +9 -1
- package/src/lib/version.ts +16 -0
- package/src/lib/view.ts +9 -10
- package/src/middleware/__tests__/auth.test.ts +6 -28
- package/src/middleware/__tests__/onboarding.test.ts +1 -1
- package/src/middleware/auth.ts +6 -12
- package/src/middleware/config.ts +51 -0
- package/src/middleware/error-handler.ts +56 -0
- package/src/middleware/onboarding.ts +1 -1
- package/src/preset.css +6 -0
- package/src/routes/__tests__/compose.test.ts +104 -17
- package/src/routes/api/__tests__/collections.test.ts +93 -2
- package/src/routes/api/__tests__/posts.test.ts +2 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/collections.ts +64 -68
- package/src/routes/api/nav-items.ts +21 -59
- package/src/routes/api/pages.ts +18 -46
- package/src/routes/api/posts.ts +64 -86
- package/src/routes/api/search.ts +6 -4
- package/src/routes/api/settings.ts +8 -24
- package/src/routes/api/upload.ts +55 -53
- package/src/routes/auth/__tests__/setup.test.ts +118 -0
- package/src/routes/auth/reset.tsx +17 -66
- package/src/routes/auth/setup.tsx +67 -11
- package/src/routes/auth/signin.tsx +44 -8
- package/src/routes/compose.tsx +194 -0
- package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
- package/src/routes/dash/__tests__/pages.test.ts +2 -2
- package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
- package/src/routes/dash/appearance.tsx +173 -0
- package/src/routes/dash/collections.tsx +80 -14
- package/src/routes/dash/index.tsx +12 -14
- package/src/routes/dash/media.tsx +46 -49
- package/src/routes/dash/pages.tsx +85 -37
- package/src/routes/dash/posts.tsx +60 -23
- package/src/routes/dash/redirects.tsx +43 -33
- package/src/routes/dash/settings.tsx +234 -214
- package/src/routes/feed/__tests__/rss.test.ts +7 -3
- package/src/routes/feed/rss.ts +11 -16
- package/src/routes/feed/sitemap.ts +15 -9
- package/src/routes/pages/__tests__/collections.test.ts +9 -8
- package/src/routes/pages/archive.tsx +2 -2
- package/src/routes/pages/collection.tsx +76 -9
- package/src/routes/pages/collections.tsx +3 -1
- package/src/routes/pages/featured.tsx +2 -2
- package/src/routes/pages/home.tsx +3 -3
- package/src/routes/pages/latest.tsx +2 -2
- package/src/routes/pages/page.tsx +2 -2
- package/src/routes/pages/post.tsx +2 -2
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +324 -34
- package/src/services/__tests__/media.test.ts +1 -1
- package/src/services/__tests__/page.test.ts +116 -1
- package/src/services/auth.ts +88 -0
- package/src/services/collection.ts +169 -30
- package/src/services/index.ts +8 -3
- package/src/services/media.ts +39 -12
- package/src/services/navigation.ts +17 -5
- package/src/services/page.ts +24 -4
- package/src/services/post.ts +87 -19
- package/src/services/search.ts +0 -1
- package/src/services/settings.ts +21 -13
- package/src/style.css +3 -0
- package/src/styles/components.css +42 -1
- package/src/styles/tokens.css +4 -0
- package/src/styles/ui.css +902 -73
- package/src/types/app-context.ts +25 -0
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +60 -23
- package/src/types/entities.ts +12 -2
- package/src/types/lingui-react-macro.d.ts +3 -3
- package/src/types/operations.ts +2 -4
- package/src/types/views.ts +1 -3
- package/src/ui/__tests__/font-themes.test.ts +27 -8
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
- package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
- package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
- package/src/ui/components/collection-types.ts +45 -0
- package/src/ui/components/compose-types.ts +75 -0
- package/src/ui/components/jant-collection-form.ts +512 -0
- package/src/ui/components/jant-compose-dialog.ts +494 -0
- package/src/ui/components/jant-compose-editor.ts +799 -0
- package/src/ui/components/jant-post-form.ts +290 -0
- package/src/ui/components/jant-settings-avatar.ts +231 -0
- package/src/ui/components/jant-settings-general.ts +436 -0
- package/src/ui/components/post-form-template.ts +260 -0
- package/src/ui/components/post-form-types.ts +87 -0
- package/src/ui/components/settings-types.ts +62 -0
- package/src/ui/compose/ComposeDialog.tsx +141 -385
- package/src/ui/compose/ComposePrompt.tsx +3 -3
- package/src/ui/dash/PostList.tsx +55 -61
- package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
- package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
- package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
- package/src/ui/dash/collections/CollectionForm.tsx +130 -117
- package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
- package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
- package/src/ui/dash/index.ts +1 -1
- package/src/ui/dash/posts/PostForm.tsx +248 -0
- package/src/ui/dash/settings/AccountContent.tsx +69 -80
- package/src/ui/dash/settings/GeneralContent.tsx +159 -478
- package/src/ui/dash/settings/SettingsNav.tsx +4 -4
- package/src/ui/font-themes.ts +115 -32
- package/src/ui/layouts/BaseLayout.tsx +49 -19
- package/src/ui/layouts/DashLayout.tsx +14 -9
- package/src/ui/layouts/SiteLayout.tsx +38 -23
- package/src/ui/pages/CollectionPage.tsx +12 -2
- package/src/ui/pages/CollectionsPage.tsx +27 -27
- package/src/ui/pages/HomePage.tsx +15 -6
- package/src/ui/pages/SearchPage.tsx +1 -2
- package/src/ui/shared/CollectionsSidebar.tsx +59 -0
- package/src/ui/shared/Pagination.tsx +2 -2
- package/dist/app.js +0 -265
- package/dist/auth.js +0 -36
- package/dist/client.js +0 -13
- package/dist/db/index.js +0 -10
- package/dist/db/schema.js +0 -224
- package/dist/i18n/Trans.js +0 -24
- package/dist/i18n/context.js +0 -58
- package/dist/i18n/detect.js +0 -26
- package/dist/i18n/i18n.js +0 -49
- package/dist/i18n/index.js +0 -44
- package/dist/i18n/locales/en.js +0 -1
- package/dist/i18n/locales/zh-Hans.js +0 -1
- package/dist/i18n/locales/zh-Hant.js +0 -1
- package/dist/i18n/locales.js +0 -13
- package/dist/i18n/middleware.js +0 -30
- package/dist/lib/avatar-upload.js +0 -134
- package/dist/lib/config.js +0 -143
- package/dist/lib/constants.js +0 -50
- package/dist/lib/excerpt.js +0 -76
- package/dist/lib/favicon.js +0 -102
- package/dist/lib/feed.js +0 -123
- package/dist/lib/image-processor.js +0 -187
- package/dist/lib/image.js +0 -97
- package/dist/lib/index.js +0 -7
- package/dist/lib/markdown.js +0 -83
- package/dist/lib/media-helpers.js +0 -49
- package/dist/lib/media-upload.js +0 -104
- package/dist/lib/nav-reorder.js +0 -27
- package/dist/lib/navigation.js +0 -79
- package/dist/lib/pagination.js +0 -44
- package/dist/lib/render.js +0 -53
- package/dist/lib/schemas.js +0 -174
- package/dist/lib/sqid.js +0 -72
- package/dist/lib/sse.js +0 -218
- package/dist/lib/storage.js +0 -164
- package/dist/lib/theme.js +0 -65
- package/dist/lib/time.js +0 -159
- package/dist/lib/timeline.js +0 -95
- package/dist/lib/timezones.js +0 -388
- package/dist/lib/url.js +0 -89
- package/dist/lib/view.js +0 -217
- package/dist/middleware/auth.js +0 -52
- package/dist/middleware/onboarding.js +0 -41
- package/dist/routes/api/collections.js +0 -124
- package/dist/routes/api/nav-items.js +0 -104
- package/dist/routes/api/pages.js +0 -91
- package/dist/routes/api/posts.js +0 -218
- package/dist/routes/api/search.js +0 -48
- package/dist/routes/api/settings.js +0 -68
- package/dist/routes/api/upload.js +0 -246
- package/dist/routes/auth/reset.js +0 -221
- package/dist/routes/auth/setup.js +0 -194
- package/dist/routes/auth/signin.js +0 -176
- package/dist/routes/compose.js +0 -48
- package/dist/routes/dash/collections.js +0 -115
- package/dist/routes/dash/index.js +0 -118
- package/dist/routes/dash/media.js +0 -106
- package/dist/routes/dash/pages.js +0 -294
- package/dist/routes/dash/posts.js +0 -244
- package/dist/routes/dash/redirects.js +0 -257
- package/dist/routes/dash/settings.js +0 -379
- package/dist/routes/feed/rss.js +0 -62
- package/dist/routes/feed/sitemap.js +0 -49
- package/dist/routes/pages/archive.js +0 -62
- package/dist/routes/pages/collection.js +0 -34
- package/dist/routes/pages/collections.js +0 -28
- package/dist/routes/pages/featured.js +0 -36
- package/dist/routes/pages/home.js +0 -64
- package/dist/routes/pages/latest.js +0 -45
- package/dist/routes/pages/page.js +0 -68
- package/dist/routes/pages/post.js +0 -44
- package/dist/routes/pages/search.js +0 -54
- package/dist/services/collection.js +0 -109
- package/dist/services/index.js +0 -24
- package/dist/services/media.js +0 -117
- package/dist/services/navigation.js +0 -91
- package/dist/services/page.js +0 -84
- package/dist/services/post.js +0 -229
- package/dist/services/redirect.js +0 -48
- package/dist/services/search.js +0 -67
- package/dist/services/settings.js +0 -68
- package/dist/types/bindings.js +0 -3
- package/dist/types/config.js +0 -147
- package/dist/types/constants.js +0 -27
- package/dist/types/entities.js +0 -3
- package/dist/types/lingui-react-macro.d.js +0 -9
- package/dist/types/operations.js +0 -3
- package/dist/types/props.js +0 -3
- package/dist/types/sortablejs.d.js +0 -5
- package/dist/types/views.js +0 -5
- package/dist/types.js +0 -11
- package/dist/ui/color-themes.js +0 -268
- package/dist/ui/compose/ComposeDialog.js +0 -467
- package/dist/ui/compose/ComposePrompt.js +0 -55
- package/dist/ui/dash/ActionButtons.js +0 -46
- package/dist/ui/dash/CrudPageHeader.js +0 -22
- package/dist/ui/dash/DangerZone.js +0 -36
- package/dist/ui/dash/FormatBadge.js +0 -27
- package/dist/ui/dash/ListItemRow.js +0 -21
- package/dist/ui/dash/PageForm.js +0 -195
- package/dist/ui/dash/PostForm.js +0 -395
- package/dist/ui/dash/PostList.js +0 -83
- package/dist/ui/dash/StatusBadge.js +0 -46
- package/dist/ui/dash/collections/CollectionForm.js +0 -152
- package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
- package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
- package/dist/ui/dash/index.js +0 -10
- package/dist/ui/dash/media/MediaListContent.js +0 -166
- package/dist/ui/dash/media/ViewMediaContent.js +0 -212
- package/dist/ui/dash/pages/LinkFormContent.js +0 -130
- package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
- package/dist/ui/dash/settings/AccountContent.js +0 -209
- package/dist/ui/dash/settings/AppearanceContent.js +0 -259
- package/dist/ui/dash/settings/GeneralContent.js +0 -536
- package/dist/ui/dash/settings/SettingsNav.js +0 -41
- package/dist/ui/feed/LinkCard.js +0 -72
- package/dist/ui/feed/NoteCard.js +0 -58
- package/dist/ui/feed/QuoteCard.js +0 -63
- package/dist/ui/feed/ThreadPreview.js +0 -48
- package/dist/ui/feed/TimelineFeed.js +0 -41
- package/dist/ui/feed/TimelineItem.js +0 -27
- package/dist/ui/font-themes.js +0 -36
- package/dist/ui/layouts/BaseLayout.js +0 -153
- package/dist/ui/layouts/DashLayout.js +0 -141
- package/dist/ui/layouts/SiteLayout.js +0 -169
- package/dist/ui/pages/ArchivePage.js +0 -143
- package/dist/ui/pages/CollectionPage.js +0 -70
- package/dist/ui/pages/CollectionsPage.js +0 -76
- package/dist/ui/pages/FeaturedPage.js +0 -24
- package/dist/ui/pages/HomePage.js +0 -24
- package/dist/ui/pages/PostPage.js +0 -55
- package/dist/ui/pages/SearchPage.js +0 -122
- package/dist/ui/pages/SinglePage.js +0 -23
- package/dist/ui/shared/EmptyState.js +0 -27
- package/dist/ui/shared/MediaGallery.js +0 -35
- package/dist/ui/shared/Pagination.js +0 -195
- package/dist/ui/shared/ThreadView.js +0 -108
- package/dist/ui/shared/index.js +0 -5
- package/dist/vendor/datastar.js +0 -1606
- package/src/lib/__tests__/config.test.ts +0 -192
- package/src/lib/config.ts +0 -167
- package/src/routes/compose.ts +0 -63
- package/src/ui/dash/PostForm.tsx +0 -360
- package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
package/dist/lib/media-upload.js
DELETED
|
@@ -1,104 +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
|
-
*/ import { ImageProcessor } from "./image-processor.js";
|
|
11
|
-
/**
|
|
12
|
-
* Format file size for display
|
|
13
|
-
*/ function formatFileSize(bytes) {
|
|
14
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
15
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
16
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Ensure the media grid exists, removing empty state if needed
|
|
20
|
-
*/ function ensureGridExists() {
|
|
21
|
-
let grid = document.getElementById("media-grid");
|
|
22
|
-
if (grid) return grid;
|
|
23
|
-
document.getElementById("empty-state")?.remove();
|
|
24
|
-
grid = document.createElement("div");
|
|
25
|
-
grid.id = "media-grid";
|
|
26
|
-
grid.className = "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4";
|
|
27
|
-
document.getElementById("media-content")?.appendChild(grid);
|
|
28
|
-
return grid;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Create a placeholder card with spinner in the media grid
|
|
32
|
-
*/ function createPlaceholder(fileName, fileSize, statusText) {
|
|
33
|
-
const placeholder = document.createElement("div");
|
|
34
|
-
placeholder.id = "upload-placeholder";
|
|
35
|
-
placeholder.className = "group relative";
|
|
36
|
-
placeholder.innerHTML = `
|
|
37
|
-
<div class="aspect-square bg-muted rounded-lg overflow-hidden border flex items-center justify-center">
|
|
38
|
-
<div class="text-center px-2">
|
|
39
|
-
<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">
|
|
40
|
-
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
41
|
-
<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>
|
|
42
|
-
</svg>
|
|
43
|
-
<span id="upload-status" class="text-xs text-muted-foreground">${statusText}</span>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
<div class="mt-2 text-xs truncate" title="${fileName}">${fileName}</div>
|
|
47
|
-
<div class="text-xs text-muted-foreground">${formatFileSize(fileSize)}</div>
|
|
48
|
-
`;
|
|
49
|
-
return placeholder;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Replace placeholder content with an error message
|
|
53
|
-
*/ function showPlaceholderError(placeholder, fileName, errorMessage) {
|
|
54
|
-
placeholder.innerHTML = `
|
|
55
|
-
<div class="aspect-square bg-destructive/10 rounded-lg overflow-hidden border border-destructive flex items-center justify-center">
|
|
56
|
-
<div class="text-center px-2">
|
|
57
|
-
<span class="text-xs text-destructive">${errorMessage}</span>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
<div class="mt-2 text-xs truncate text-destructive">${fileName}</div>
|
|
61
|
-
<button type="button" class="text-xs text-muted-foreground hover:underline" onclick="this.closest('.group').remove()">Remove</button>
|
|
62
|
-
`;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Handle the upload flow for a selected file
|
|
66
|
-
*/ async function handleUpload(input, file) {
|
|
67
|
-
const processingText = input.dataset.textProcessing || "Processing...";
|
|
68
|
-
const uploadingText = input.dataset.textUploading || "Uploading...";
|
|
69
|
-
const errorText = input.dataset.textError || "Upload failed. Please try again.";
|
|
70
|
-
const grid = ensureGridExists();
|
|
71
|
-
const placeholder = createPlaceholder(file.name, file.size, processingText);
|
|
72
|
-
grid.prepend(placeholder);
|
|
73
|
-
try {
|
|
74
|
-
// Process image client-side (resize, convert to WebP)
|
|
75
|
-
const processed = await ImageProcessor.processToFile(file);
|
|
76
|
-
// Update status
|
|
77
|
-
const statusEl = document.getElementById("upload-status");
|
|
78
|
-
if (statusEl) statusEl.textContent = uploadingText;
|
|
79
|
-
// Set processed file on hidden form input via DataTransfer API
|
|
80
|
-
const formInput = document.getElementById("upload-file-input");
|
|
81
|
-
const form = document.getElementById("upload-form");
|
|
82
|
-
if (!formInput || !form) throw new Error("Upload form not found");
|
|
83
|
-
const dt = new DataTransfer();
|
|
84
|
-
dt.items.add(processed);
|
|
85
|
-
formInput.files = dt.files;
|
|
86
|
-
// Trigger Datastar-intercepted form submission
|
|
87
|
-
form.requestSubmit();
|
|
88
|
-
} catch (err) {
|
|
89
|
-
const message = err instanceof Error ? err.message : errorText;
|
|
90
|
-
showPlaceholderError(placeholder, file.name, message);
|
|
91
|
-
}
|
|
92
|
-
// Reset file input so the same file can be re-selected
|
|
93
|
-
input.value = "";
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Initialize media upload via event delegation
|
|
97
|
-
*/ function initMediaUpload() {
|
|
98
|
-
document.addEventListener("change", (e)=>{
|
|
99
|
-
const input = e.target.closest("[data-media-upload]");
|
|
100
|
-
if (!input?.files?.[0]) return;
|
|
101
|
-
handleUpload(input, input.files[0]);
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
initMediaUpload();
|
package/dist/lib/nav-reorder.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Navigation Link Reorder
|
|
3
|
-
*
|
|
4
|
-
* Initializes SortableJS on the navigation links list in the dashboard.
|
|
5
|
-
* Auto-detects the list element and only activates when present.
|
|
6
|
-
*/ import Sortable from "sortablejs";
|
|
7
|
-
const list = document.getElementById("nav-links-list");
|
|
8
|
-
if (list) {
|
|
9
|
-
Sortable.create(list, {
|
|
10
|
-
animation: 150,
|
|
11
|
-
handle: "[data-id]",
|
|
12
|
-
onEnd () {
|
|
13
|
-
const ids = [
|
|
14
|
-
...list.querySelectorAll("[data-id]")
|
|
15
|
-
].map((el)=>Number(el.dataset.id));
|
|
16
|
-
fetch("/dash/pages/reorder", {
|
|
17
|
-
method: "POST",
|
|
18
|
-
headers: {
|
|
19
|
-
"Content-Type": "application/json"
|
|
20
|
-
},
|
|
21
|
-
body: JSON.stringify({
|
|
22
|
-
ids
|
|
23
|
-
})
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
}
|
package/dist/lib/navigation.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Navigation Helper
|
|
3
|
-
*
|
|
4
|
-
* Provides shared data fetching for public page navigation.
|
|
5
|
-
*/ import { getSiteName, getHomeDefaultView, getSiteFooter } from "./config.js";
|
|
6
|
-
import { toNavItemViews } from "./view.js";
|
|
7
|
-
import { getMediaUrl, getPublicUrlForProvider } from "./image.js";
|
|
8
|
-
import { render as renderMarkdown } from "./markdown.js";
|
|
9
|
-
/**
|
|
10
|
-
* Fetch navigation data for public pages.
|
|
11
|
-
*
|
|
12
|
-
* Returns NavItemView[] with pre-computed isActive/isExternal state.
|
|
13
|
-
* Also checks authentication status and loads collections for authenticated users.
|
|
14
|
-
*
|
|
15
|
-
* @param c - Hono context
|
|
16
|
-
* @returns Navigation data for SiteLayout
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```typescript
|
|
20
|
-
* const navData = await getNavigationData(c);
|
|
21
|
-
* return renderPublicPage(c, {
|
|
22
|
-
* title: "My Page",
|
|
23
|
-
* navData,
|
|
24
|
-
* content: <MyContent />,
|
|
25
|
-
* });
|
|
26
|
-
* ```
|
|
27
|
-
*/ export async function getNavigationData(c) {
|
|
28
|
-
const items = await c.var.services.navItems.list();
|
|
29
|
-
const currentPath = new URL(c.req.url).pathname;
|
|
30
|
-
const [siteName, homeDefaultView, siteFooter] = await Promise.all([
|
|
31
|
-
getSiteName(c),
|
|
32
|
-
getHomeDefaultView(c),
|
|
33
|
-
getSiteFooter(c)
|
|
34
|
-
]);
|
|
35
|
-
// Only include description if explicitly set (DB or env), not the default
|
|
36
|
-
const dbDescription = await c.var.services.settings.get("SITE_DESCRIPTION");
|
|
37
|
-
const envDescription = c.env.SITE_DESCRIPTION;
|
|
38
|
-
const siteDescription = dbDescription || (typeof envDescription === "string" ? envDescription : "");
|
|
39
|
-
// Resolve avatar URL from storage key
|
|
40
|
-
const avatarKey = await c.var.services.settings.get("SITE_AVATAR");
|
|
41
|
-
const showHeaderAvatar = await c.var.services.settings.get("SHOW_HEADER_AVATAR") === "true";
|
|
42
|
-
let siteAvatarUrl;
|
|
43
|
-
if (avatarKey) {
|
|
44
|
-
const publicUrl = getPublicUrlForProvider(c.env.STORAGE_DRIVER || "r2", c.env.R2_PUBLIC_URL, c.env.S3_PUBLIC_URL);
|
|
45
|
-
siteAvatarUrl = getMediaUrl(avatarKey, publicUrl);
|
|
46
|
-
}
|
|
47
|
-
// Render footer markdown
|
|
48
|
-
const siteFooterHtml = siteFooter ? renderMarkdown(siteFooter) : undefined;
|
|
49
|
-
const links = toNavItemViews(items, currentPath);
|
|
50
|
-
// Check auth status for compose button
|
|
51
|
-
let isAuthenticated = false;
|
|
52
|
-
let collections = [];
|
|
53
|
-
if (c.var.auth) {
|
|
54
|
-
try {
|
|
55
|
-
const session = await c.var.auth.api.getSession({
|
|
56
|
-
headers: c.req.raw.headers
|
|
57
|
-
});
|
|
58
|
-
isAuthenticated = !!session?.user;
|
|
59
|
-
} catch {
|
|
60
|
-
// Not authenticated
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Only load collections when authenticated (for compose dialog)
|
|
64
|
-
if (isAuthenticated) {
|
|
65
|
-
collections = await c.var.services.collections.list();
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
links,
|
|
69
|
-
currentPath,
|
|
70
|
-
siteName,
|
|
71
|
-
siteDescription,
|
|
72
|
-
isAuthenticated,
|
|
73
|
-
collections,
|
|
74
|
-
homeDefaultView,
|
|
75
|
-
siteAvatarUrl,
|
|
76
|
-
showHeaderAvatar: showHeaderAvatar && !!siteAvatarUrl,
|
|
77
|
-
siteFooterHtml
|
|
78
|
-
};
|
|
79
|
-
}
|
package/dist/lib/pagination.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pagination Utilities
|
|
3
|
-
*
|
|
4
|
-
* Pure utility functions for page-based pagination.
|
|
5
|
-
*/ /**
|
|
6
|
-
* Computes which page numbers to display in a numbered pagination control.
|
|
7
|
-
* Always includes: first page, last page, current page, and 1 page on each side of current.
|
|
8
|
-
* Gaps between non-consecutive pages are represented by 0 (ellipsis marker).
|
|
9
|
-
*
|
|
10
|
-
* @param currentPage - The current active page (1-indexed)
|
|
11
|
-
* @param totalPages - Total number of pages
|
|
12
|
-
* @returns Array of page numbers, with 0 representing ellipsis gaps
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```ts
|
|
16
|
-
* getPageNumbers(1, 5) // [1, 2, 3, 4, 5]
|
|
17
|
-
* getPageNumbers(1, 20) // [1, 2, 0, 20]
|
|
18
|
-
* getPageNumbers(10, 20) // [1, 0, 9, 10, 11, 0, 20]
|
|
19
|
-
* ```
|
|
20
|
-
*/ export function getPageNumbers(currentPage, totalPages) {
|
|
21
|
-
if (totalPages <= 7) {
|
|
22
|
-
return Array.from({
|
|
23
|
-
length: totalPages
|
|
24
|
-
}, (_, i)=>i + 1);
|
|
25
|
-
}
|
|
26
|
-
const pages = new Set();
|
|
27
|
-
pages.add(1);
|
|
28
|
-
pages.add(totalPages);
|
|
29
|
-
pages.add(currentPage);
|
|
30
|
-
if (currentPage > 1) pages.add(currentPage - 1);
|
|
31
|
-
if (currentPage < totalPages) pages.add(currentPage + 1);
|
|
32
|
-
const sorted = [
|
|
33
|
-
...pages
|
|
34
|
-
].sort((a, b)=>a - b);
|
|
35
|
-
// Insert 0 for gaps
|
|
36
|
-
const result = [];
|
|
37
|
-
for(let i = 0; i < sorted.length; i++){
|
|
38
|
-
if (i > 0 && sorted[i] - sorted[i - 1] > 1) {
|
|
39
|
-
result.push(0); // ellipsis marker
|
|
40
|
-
}
|
|
41
|
-
result.push(sorted[i]);
|
|
42
|
-
}
|
|
43
|
-
return result;
|
|
44
|
-
}
|
package/dist/lib/render.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public Page Rendering Helper
|
|
3
|
-
*
|
|
4
|
-
* Provides a single entry point for rendering public pages with the
|
|
5
|
-
* correct layout stack: BaseLayout > SiteLayout > content.
|
|
6
|
-
*/ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
7
|
-
import { BaseLayout } from "../ui/layouts/BaseLayout.js";
|
|
8
|
-
import { SiteLayout } from "../ui/layouts/SiteLayout.js";
|
|
9
|
-
/**
|
|
10
|
-
* Render a public page with the standard layout stack.
|
|
11
|
-
*
|
|
12
|
-
* @param c - Hono context
|
|
13
|
-
* @param options - Page rendering options
|
|
14
|
-
* @returns Hono HTML response
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```typescript
|
|
18
|
-
* const navData = await getNavigationData(c);
|
|
19
|
-
* return renderPublicPage(c, {
|
|
20
|
-
* title: "My Page",
|
|
21
|
-
* navData,
|
|
22
|
-
* content: <MyPageComponent />,
|
|
23
|
-
* });
|
|
24
|
-
* ```
|
|
25
|
-
*/ export function renderPublicPage(c, options) {
|
|
26
|
-
const { title, description, navData, content } = options;
|
|
27
|
-
const layoutProps = {
|
|
28
|
-
siteName: navData.siteName,
|
|
29
|
-
siteDescription: navData.siteDescription,
|
|
30
|
-
links: navData.links,
|
|
31
|
-
currentPath: navData.currentPath,
|
|
32
|
-
isAuthenticated: navData.isAuthenticated,
|
|
33
|
-
collections: navData.collections,
|
|
34
|
-
homeDefaultView: navData.homeDefaultView,
|
|
35
|
-
siteAvatarUrl: navData.siteAvatarUrl,
|
|
36
|
-
showHeaderAvatar: navData.showHeaderAvatar,
|
|
37
|
-
siteFooterHtml: navData.siteFooterHtml
|
|
38
|
-
};
|
|
39
|
-
// Read favicon and noindex from context (set by theme middleware)
|
|
40
|
-
const faviconUrl = c.get("faviconUrl");
|
|
41
|
-
const noindex = c.get("noindex");
|
|
42
|
-
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
43
|
-
title: title,
|
|
44
|
-
description: description,
|
|
45
|
-
c: c,
|
|
46
|
-
faviconUrl: faviconUrl,
|
|
47
|
-
noindex: noindex,
|
|
48
|
-
children: /*#__PURE__*/ _jsx(SiteLayout, {
|
|
49
|
-
...layoutProps,
|
|
50
|
-
children: content
|
|
51
|
-
})
|
|
52
|
-
}));
|
|
53
|
-
}
|
package/dist/lib/schemas.js
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared Zod schemas for validation (v2)
|
|
3
|
-
*
|
|
4
|
-
* These schemas ensure type-safe validation of user input
|
|
5
|
-
* from forms, API requests, and other external sources.
|
|
6
|
-
*
|
|
7
|
-
* IMPORTANT: Types are defined in types.ts as the single source of truth.
|
|
8
|
-
* This file only defines Zod validation schemas based on those types.
|
|
9
|
-
*/ import { z } from "zod";
|
|
10
|
-
import { FORMATS, STATUSES, SORT_ORDERS, NAV_ITEM_TYPES, MAX_MEDIA_ATTACHMENTS } from "../types.js";
|
|
11
|
-
/**
|
|
12
|
-
* Post format enum schema
|
|
13
|
-
* Based on FORMATS from types.ts
|
|
14
|
-
*/ export const FormatSchema = z.enum(FORMATS);
|
|
15
|
-
/**
|
|
16
|
-
* Post status enum schema
|
|
17
|
-
* Based on STATUSES from types.ts
|
|
18
|
-
*/ export const StatusSchema = z.enum(STATUSES);
|
|
19
|
-
/**
|
|
20
|
-
* Collection sort order enum schema
|
|
21
|
-
*/ export const SortOrderSchema = z.enum(SORT_ORDERS);
|
|
22
|
-
/**
|
|
23
|
-
* Navigation item type enum schema
|
|
24
|
-
*/ export const NavItemTypeSchema = z.enum(NAV_ITEM_TYPES);
|
|
25
|
-
/**
|
|
26
|
-
* Redirect type enum schema
|
|
27
|
-
* Form input validation for redirect type (stored as number in DB)
|
|
28
|
-
*/ export const RedirectTypeSchema = z.enum([
|
|
29
|
-
"301",
|
|
30
|
-
"302"
|
|
31
|
-
]);
|
|
32
|
-
/**
|
|
33
|
-
* Rating schema (1-5 integer)
|
|
34
|
-
*/ export const RatingSchema = z.coerce.number().int().min(0).max(5).optional().or(z.literal("").transform(()=>undefined)).transform((v)=>v === 0 ? undefined : v);
|
|
35
|
-
/**
|
|
36
|
-
* API request body schema for creating a post
|
|
37
|
-
*/ export const CreatePostSchema = z.object({
|
|
38
|
-
format: FormatSchema,
|
|
39
|
-
path: z.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/).optional().or(z.literal("").transform(()=>undefined)),
|
|
40
|
-
title: z.string().optional(),
|
|
41
|
-
body: z.string().optional(),
|
|
42
|
-
status: StatusSchema.optional(),
|
|
43
|
-
featured: z.union([
|
|
44
|
-
z.boolean(),
|
|
45
|
-
z.literal("on").transform(()=>true)
|
|
46
|
-
]).optional(),
|
|
47
|
-
pinned: z.union([
|
|
48
|
-
z.boolean(),
|
|
49
|
-
z.literal("on").transform(()=>true)
|
|
50
|
-
]).optional(),
|
|
51
|
-
url: z.url().optional().or(z.literal("")),
|
|
52
|
-
quoteText: z.string().optional(),
|
|
53
|
-
rating: RatingSchema,
|
|
54
|
-
collectionId: z.coerce.number().int().min(0).optional().or(z.literal("").transform(()=>undefined)).transform((v)=>v === 0 ? undefined : v),
|
|
55
|
-
replyToId: z.string().optional(),
|
|
56
|
-
publishedAt: z.number().int().positive().optional(),
|
|
57
|
-
mediaIds: z.array(z.string()).max(MAX_MEDIA_ATTACHMENTS).optional()
|
|
58
|
-
});
|
|
59
|
-
/**
|
|
60
|
-
* API request body schema for updating a post
|
|
61
|
-
*/ export const UpdatePostSchema = CreatePostSchema.partial();
|
|
62
|
-
/**
|
|
63
|
-
* API request body schema for creating a page
|
|
64
|
-
*/ export const CreatePageSchema = z.object({
|
|
65
|
-
slug: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/),
|
|
66
|
-
title: z.string().optional(),
|
|
67
|
-
body: z.string().optional(),
|
|
68
|
-
status: StatusSchema.optional()
|
|
69
|
-
});
|
|
70
|
-
/**
|
|
71
|
-
* API request body schema for updating a page
|
|
72
|
-
*/ export const UpdatePageSchema = CreatePageSchema.partial();
|
|
73
|
-
/**
|
|
74
|
-
* API request body schema for creating a navigation item
|
|
75
|
-
*/ export const CreateNavItemSchema = z.object({
|
|
76
|
-
type: NavItemTypeSchema,
|
|
77
|
-
label: z.string().min(1),
|
|
78
|
-
url: z.string().min(1),
|
|
79
|
-
pageId: z.coerce.number().int().positive().optional(),
|
|
80
|
-
position: z.coerce.number().int().min(0).optional()
|
|
81
|
-
});
|
|
82
|
-
/**
|
|
83
|
-
* API request body schema for updating a navigation item
|
|
84
|
-
*/ export const UpdateNavItemSchema = CreateNavItemSchema.partial();
|
|
85
|
-
/**
|
|
86
|
-
* API request body schema for creating a collection
|
|
87
|
-
*/ export const CreateCollectionSchema = z.object({
|
|
88
|
-
slug: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/),
|
|
89
|
-
title: z.string().min(1),
|
|
90
|
-
description: z.string().optional(),
|
|
91
|
-
icon: z.string().optional(),
|
|
92
|
-
sortOrder: SortOrderSchema.optional(),
|
|
93
|
-
position: z.coerce.number().int().min(0).optional(),
|
|
94
|
-
showDivider: z.union([
|
|
95
|
-
z.boolean(),
|
|
96
|
-
z.literal("on").transform(()=>true)
|
|
97
|
-
]).optional()
|
|
98
|
-
});
|
|
99
|
-
/**
|
|
100
|
-
* API request body schema for updating a collection
|
|
101
|
-
*/ export const UpdateCollectionSchema = CreateCollectionSchema.partial();
|
|
102
|
-
// =============================================================================
|
|
103
|
-
// Auth Schemas
|
|
104
|
-
// =============================================================================
|
|
105
|
-
/**
|
|
106
|
-
* Setup form validation schema
|
|
107
|
-
*/ export const SetupSchema = z.object({
|
|
108
|
-
name: z.string().min(1, "Name is required"),
|
|
109
|
-
email: z.string().email("Invalid email address"),
|
|
110
|
-
password: z.string().min(8, "Password must be at least 8 characters")
|
|
111
|
-
});
|
|
112
|
-
/**
|
|
113
|
-
* Sign-in form validation schema
|
|
114
|
-
*/ export const SigninSchema = z.object({
|
|
115
|
-
email: z.string().email("Invalid email address"),
|
|
116
|
-
password: z.string().min(1, "Password is required")
|
|
117
|
-
});
|
|
118
|
-
/**
|
|
119
|
-
* Password reset form validation schema
|
|
120
|
-
*/ export const ResetPasswordSchema = z.object({
|
|
121
|
-
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
122
|
-
confirmPassword: z.string().min(1),
|
|
123
|
-
token: z.string().min(1)
|
|
124
|
-
}).refine((d)=>d.password === d.confirmPassword, {
|
|
125
|
-
message: "Passwords do not match",
|
|
126
|
-
path: [
|
|
127
|
-
"confirmPassword"
|
|
128
|
-
]
|
|
129
|
-
});
|
|
130
|
-
// =============================================================================
|
|
131
|
-
// Form Data Helpers
|
|
132
|
-
// =============================================================================
|
|
133
|
-
/**
|
|
134
|
-
* Form data helper: safely parse a FormData value with a schema
|
|
135
|
-
*
|
|
136
|
-
* @example
|
|
137
|
-
* ```ts
|
|
138
|
-
* const format = parseFormData(formData, "format", FormatSchema);
|
|
139
|
-
* // format is Format, throws if invalid
|
|
140
|
-
* ```
|
|
141
|
-
*/ export function parseFormData(formData, key, schema) {
|
|
142
|
-
const value = formData.get(key);
|
|
143
|
-
if (value === null) {
|
|
144
|
-
throw new Error(`Missing required field: ${key}`);
|
|
145
|
-
}
|
|
146
|
-
return schema.parse(value);
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Form data helper: safely parse optional FormData value with a schema
|
|
150
|
-
*
|
|
151
|
-
* @example
|
|
152
|
-
* ```ts
|
|
153
|
-
* const slug = parseFormDataOptional(formData, "slug", z.string());
|
|
154
|
-
* // slug is string | undefined
|
|
155
|
-
* ```
|
|
156
|
-
*/ export function parseFormDataOptional(formData, key, schema) {
|
|
157
|
-
const value = formData.get(key);
|
|
158
|
-
if (value === null || value === "") {
|
|
159
|
-
return undefined;
|
|
160
|
-
}
|
|
161
|
-
return schema.parse(value);
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Validates media attachment count for a post.
|
|
165
|
-
* All formats allow 0-20 media attachments.
|
|
166
|
-
*
|
|
167
|
-
* @param mediaIds - Array of media IDs to attach
|
|
168
|
-
* @returns null if valid, error string if invalid
|
|
169
|
-
*/ export function validateMediaCount(mediaIds) {
|
|
170
|
-
if (mediaIds.length > MAX_MEDIA_ATTACHMENTS) {
|
|
171
|
-
return `Posts allow at most ${MAX_MEDIA_ATTACHMENTS} media attachments`;
|
|
172
|
-
}
|
|
173
|
-
return null;
|
|
174
|
-
}
|
package/dist/lib/sqid.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sqids - Short unique IDs for URLs
|
|
3
|
-
*
|
|
4
|
-
* Encodes numeric IDs to short strings like "jR3k"
|
|
5
|
-
*/ import Sqids from "sqids";
|
|
6
|
-
const sqids = new Sqids({
|
|
7
|
-
minLength: 4
|
|
8
|
-
});
|
|
9
|
-
/**
|
|
10
|
-
* Encodes a numeric database ID to a short, URL-friendly string.
|
|
11
|
-
*
|
|
12
|
-
* Uses the Sqids library to generate short unique identifiers with a minimum length of 4 characters.
|
|
13
|
-
* These are used in URLs (e.g., `/p/jR3k`) to obscure sequential integer IDs while maintaining
|
|
14
|
-
* uniqueness and reversibility.
|
|
15
|
-
*
|
|
16
|
-
* @param id - The numeric database ID to encode
|
|
17
|
-
* @returns A short string representation of the ID (minimum 4 characters)
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```ts
|
|
21
|
-
* const sqid = encode(123);
|
|
22
|
-
* // Returns: "jR3k" (or similar short string)
|
|
23
|
-
* ```
|
|
24
|
-
*/ export function encode(id) {
|
|
25
|
-
return sqids.encode([
|
|
26
|
-
id
|
|
27
|
-
]);
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Decodes a sqid string back to the original numeric database ID.
|
|
31
|
-
*
|
|
32
|
-
* Attempts to decode a sqid string generated by the `encode()` function. Returns the original
|
|
33
|
-
* numeric ID if valid, or `null` if the string is not a valid sqid. This is used to extract
|
|
34
|
-
* database IDs from URL parameters.
|
|
35
|
-
*
|
|
36
|
-
* @param str - The sqid string to decode
|
|
37
|
-
* @returns The original numeric ID if valid, or `null` if decoding fails
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```ts
|
|
41
|
-
* const id = decode("jR3k");
|
|
42
|
-
* // Returns: 123
|
|
43
|
-
*
|
|
44
|
-
* const invalid = decode("invalid");
|
|
45
|
-
* // Returns: null
|
|
46
|
-
* ```
|
|
47
|
-
*/ export function decode(str) {
|
|
48
|
-
try {
|
|
49
|
-
const ids = sqids.decode(str);
|
|
50
|
-
return ids[0] ?? null;
|
|
51
|
-
} catch {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Checks if a string is a valid sqid that can be decoded.
|
|
57
|
-
*
|
|
58
|
-
* Validates whether a string can be successfully decoded to a numeric ID.
|
|
59
|
-
* Useful for route validation and input sanitization.
|
|
60
|
-
*
|
|
61
|
-
* @param str - The string to validate
|
|
62
|
-
* @returns `true` if the string is a valid sqid, `false` otherwise
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* ```ts
|
|
66
|
-
* if (isValidSqid("jR3k")) {
|
|
67
|
-
* // Process the valid sqid
|
|
68
|
-
* }
|
|
69
|
-
* ```
|
|
70
|
-
*/ export function isValidSqid(str) {
|
|
71
|
-
return decode(str) !== null;
|
|
72
|
-
}
|