convex-cms 0.0.1
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/cli/commands/admin.d.ts +16 -0
- package/dist/cli/commands/admin.d.ts.map +1 -0
- package/dist/cli/commands/admin.js +88 -0
- package/dist/cli/commands/admin.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils/detectConvexUrl.d.ts +13 -0
- package/dist/cli/utils/detectConvexUrl.d.ts.map +1 -0
- package/dist/cli/utils/detectConvexUrl.js +48 -0
- package/dist/cli/utils/detectConvexUrl.js.map +1 -0
- package/dist/cli/utils/openBrowser.d.ts +7 -0
- package/dist/cli/utils/openBrowser.d.ts.map +1 -0
- package/dist/cli/utils/openBrowser.js +17 -0
- package/dist/cli/utils/openBrowser.js.map +1 -0
- package/dist/client/admin-config.d.ts +126 -0
- package/dist/client/admin-config.d.ts.map +1 -0
- package/dist/client/admin-config.js +117 -0
- package/dist/client/admin-config.js.map +1 -0
- package/dist/client/adminApi.d.ts +2273 -0
- package/dist/client/adminApi.d.ts.map +1 -0
- package/dist/client/adminApi.js +716 -0
- package/dist/client/adminApi.js.map +1 -0
- package/dist/client/agentTools.d.ts +933 -0
- package/dist/client/agentTools.d.ts.map +1 -0
- package/dist/client/agentTools.js +1004 -0
- package/dist/client/agentTools.js.map +1 -0
- package/dist/client/argTypes.d.ts +212 -0
- package/dist/client/argTypes.d.ts.map +1 -0
- package/dist/client/argTypes.js +5 -0
- package/dist/client/argTypes.js.map +1 -0
- package/dist/client/field-types.d.ts +55 -0
- package/dist/client/field-types.d.ts.map +1 -0
- package/dist/client/field-types.js +152 -0
- package/dist/client/field-types.js.map +1 -0
- package/dist/client/index.d.ts +189 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +668 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/queryBuilder.d.ts +765 -0
- package/dist/client/queryBuilder.d.ts.map +1 -0
- package/dist/client/queryBuilder.js +970 -0
- package/dist/client/queryBuilder.js.map +1 -0
- package/dist/client/schema/codegen.d.ts +128 -0
- package/dist/client/schema/codegen.d.ts.map +1 -0
- package/dist/client/schema/codegen.js +318 -0
- package/dist/client/schema/codegen.js.map +1 -0
- package/dist/client/schema/defineContentType.d.ts +221 -0
- package/dist/client/schema/defineContentType.d.ts.map +1 -0
- package/dist/client/schema/defineContentType.js +380 -0
- package/dist/client/schema/defineContentType.js.map +1 -0
- package/dist/client/schema/index.d.ts +85 -0
- package/dist/client/schema/index.d.ts.map +1 -0
- package/dist/client/schema/index.js +92 -0
- package/dist/client/schema/index.js.map +1 -0
- package/dist/client/schema/schemaDrift.d.ts +199 -0
- package/dist/client/schema/schemaDrift.d.ts.map +1 -0
- package/dist/client/schema/schemaDrift.js +340 -0
- package/dist/client/schema/schemaDrift.js.map +1 -0
- package/dist/client/schema/typedClient.d.ts +401 -0
- package/dist/client/schema/typedClient.d.ts.map +1 -0
- package/dist/client/schema/typedClient.js +269 -0
- package/dist/client/schema/typedClient.js.map +1 -0
- package/dist/client/schema/types.d.ts +477 -0
- package/dist/client/schema/types.d.ts.map +1 -0
- package/dist/client/schema/types.js +39 -0
- package/dist/client/schema/types.js.map +1 -0
- package/dist/client/types.d.ts +449 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +149 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client/workflows.d.ts +51 -0
- package/dist/client/workflows.d.ts.map +1 -0
- package/dist/client/workflows.js +103 -0
- package/dist/client/workflows.js.map +1 -0
- package/dist/client/wrapper.d.ts +2198 -0
- package/dist/client/wrapper.d.ts.map +1 -0
- package/dist/client/wrapper.js +2651 -0
- package/dist/client/wrapper.js.map +1 -0
- package/dist/component/_generated/api.d.ts +124 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +4321 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/auditLog.d.ts +410 -0
- package/dist/component/auditLog.d.ts.map +1 -0
- package/dist/component/auditLog.js +607 -0
- package/dist/component/auditLog.js.map +1 -0
- package/dist/component/authorization.d.ts +323 -0
- package/dist/component/authorization.d.ts.map +1 -0
- package/dist/component/authorization.js +464 -0
- package/dist/component/authorization.js.map +1 -0
- package/dist/component/authorizationHooks.d.ts +184 -0
- package/dist/component/authorizationHooks.d.ts.map +1 -0
- package/dist/component/authorizationHooks.js +521 -0
- package/dist/component/authorizationHooks.js.map +1 -0
- package/dist/component/bulkOperations.d.ts +200 -0
- package/dist/component/bulkOperations.d.ts.map +1 -0
- package/dist/component/bulkOperations.js +568 -0
- package/dist/component/bulkOperations.js.map +1 -0
- package/dist/component/contentEntries.d.ts +719 -0
- package/dist/component/contentEntries.d.ts.map +1 -0
- package/dist/component/contentEntries.js +1617 -0
- package/dist/component/contentEntries.js.map +1 -0
- package/dist/component/contentEntryMutations.d.ts +505 -0
- package/dist/component/contentEntryMutations.d.ts.map +1 -0
- package/dist/component/contentEntryMutations.js +1009 -0
- package/dist/component/contentEntryMutations.js.map +1 -0
- package/dist/component/contentEntryValidation.d.ts +115 -0
- package/dist/component/contentEntryValidation.d.ts.map +1 -0
- package/dist/component/contentEntryValidation.js +546 -0
- package/dist/component/contentEntryValidation.js.map +1 -0
- package/dist/component/contentLock.d.ts +328 -0
- package/dist/component/contentLock.d.ts.map +1 -0
- package/dist/component/contentLock.js +471 -0
- package/dist/component/contentLock.js.map +1 -0
- package/dist/component/contentTypeMigration.d.ts +411 -0
- package/dist/component/contentTypeMigration.d.ts.map +1 -0
- package/dist/component/contentTypeMigration.js +805 -0
- package/dist/component/contentTypeMigration.js.map +1 -0
- package/dist/component/contentTypeMutations.d.ts +975 -0
- package/dist/component/contentTypeMutations.d.ts.map +1 -0
- package/dist/component/contentTypeMutations.js +768 -0
- package/dist/component/contentTypeMutations.js.map +1 -0
- package/dist/component/contentTypes.d.ts +538 -0
- package/dist/component/contentTypes.d.ts.map +1 -0
- package/dist/component/contentTypes.js +304 -0
- package/dist/component/contentTypes.js.map +1 -0
- package/dist/component/convex.config.d.ts +42 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +43 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/documentTypes.d.ts +186 -0
- package/dist/component/documentTypes.d.ts.map +1 -0
- package/dist/component/documentTypes.js +23 -0
- package/dist/component/documentTypes.js.map +1 -0
- package/dist/component/eventEmitter.d.ts +281 -0
- package/dist/component/eventEmitter.d.ts.map +1 -0
- package/dist/component/eventEmitter.js +300 -0
- package/dist/component/eventEmitter.js.map +1 -0
- package/dist/component/exportImport.d.ts +1120 -0
- package/dist/component/exportImport.d.ts.map +1 -0
- package/dist/component/exportImport.js +931 -0
- package/dist/component/exportImport.js.map +1 -0
- package/dist/component/index.d.ts +28 -0
- package/dist/component/index.d.ts.map +1 -0
- package/dist/component/index.js +142 -0
- package/dist/component/index.js.map +1 -0
- package/dist/component/lib/deepReferenceResolver.d.ts +252 -0
- package/dist/component/lib/deepReferenceResolver.d.ts.map +1 -0
- package/dist/component/lib/deepReferenceResolver.js +601 -0
- package/dist/component/lib/deepReferenceResolver.js.map +1 -0
- package/dist/component/lib/errors.d.ts +306 -0
- package/dist/component/lib/errors.d.ts.map +1 -0
- package/dist/component/lib/errors.js +407 -0
- package/dist/component/lib/errors.js.map +1 -0
- package/dist/component/lib/index.d.ts +10 -0
- package/dist/component/lib/index.d.ts.map +1 -0
- package/dist/component/lib/index.js +33 -0
- package/dist/component/lib/index.js.map +1 -0
- package/dist/component/lib/mediaReferenceResolver.d.ts +217 -0
- package/dist/component/lib/mediaReferenceResolver.d.ts.map +1 -0
- package/dist/component/lib/mediaReferenceResolver.js +326 -0
- package/dist/component/lib/mediaReferenceResolver.js.map +1 -0
- package/dist/component/lib/metadataExtractor.d.ts +245 -0
- package/dist/component/lib/metadataExtractor.d.ts.map +1 -0
- package/dist/component/lib/metadataExtractor.js +548 -0
- package/dist/component/lib/metadataExtractor.js.map +1 -0
- package/dist/component/lib/mutationAuth.d.ts +95 -0
- package/dist/component/lib/mutationAuth.d.ts.map +1 -0
- package/dist/component/lib/mutationAuth.js +146 -0
- package/dist/component/lib/mutationAuth.js.map +1 -0
- package/dist/component/lib/queries.d.ts +17 -0
- package/dist/component/lib/queries.d.ts.map +1 -0
- package/dist/component/lib/queries.js +49 -0
- package/dist/component/lib/queries.js.map +1 -0
- package/dist/component/lib/ragContentChunker.d.ts +423 -0
- package/dist/component/lib/ragContentChunker.d.ts.map +1 -0
- package/dist/component/lib/ragContentChunker.js +897 -0
- package/dist/component/lib/ragContentChunker.js.map +1 -0
- package/dist/component/lib/referenceResolver.d.ts +175 -0
- package/dist/component/lib/referenceResolver.d.ts.map +1 -0
- package/dist/component/lib/referenceResolver.js +293 -0
- package/dist/component/lib/referenceResolver.js.map +1 -0
- package/dist/component/lib/slugGenerator.d.ts +71 -0
- package/dist/component/lib/slugGenerator.d.ts.map +1 -0
- package/dist/component/lib/slugGenerator.js +207 -0
- package/dist/component/lib/slugGenerator.js.map +1 -0
- package/dist/component/lib/slugUniqueness.d.ts +131 -0
- package/dist/component/lib/slugUniqueness.d.ts.map +1 -0
- package/dist/component/lib/slugUniqueness.js +229 -0
- package/dist/component/lib/slugUniqueness.js.map +1 -0
- package/dist/component/lib/softDelete.d.ts +18 -0
- package/dist/component/lib/softDelete.d.ts.map +1 -0
- package/dist/component/lib/softDelete.js +29 -0
- package/dist/component/lib/softDelete.js.map +1 -0
- package/dist/component/localeFallbackChain.d.ts +410 -0
- package/dist/component/localeFallbackChain.d.ts.map +1 -0
- package/dist/component/localeFallbackChain.js +467 -0
- package/dist/component/localeFallbackChain.js.map +1 -0
- package/dist/component/localeFields.d.ts +508 -0
- package/dist/component/localeFields.d.ts.map +1 -0
- package/dist/component/localeFields.js +592 -0
- package/dist/component/localeFields.js.map +1 -0
- package/dist/component/mediaAssetMutations.d.ts +235 -0
- package/dist/component/mediaAssetMutations.d.ts.map +1 -0
- package/dist/component/mediaAssetMutations.js +558 -0
- package/dist/component/mediaAssetMutations.js.map +1 -0
- package/dist/component/mediaAssets.d.ts +168 -0
- package/dist/component/mediaAssets.d.ts.map +1 -0
- package/dist/component/mediaAssets.js +618 -0
- package/dist/component/mediaAssets.js.map +1 -0
- package/dist/component/mediaFolderMutations.d.ts +642 -0
- package/dist/component/mediaFolderMutations.d.ts.map +1 -0
- package/dist/component/mediaFolderMutations.js +849 -0
- package/dist/component/mediaFolderMutations.js.map +1 -0
- package/dist/component/mediaUploadMutations.d.ts +136 -0
- package/dist/component/mediaUploadMutations.d.ts.map +1 -0
- package/dist/component/mediaUploadMutations.js +205 -0
- package/dist/component/mediaUploadMutations.js.map +1 -0
- package/dist/component/mediaVariantMutations.d.ts +468 -0
- package/dist/component/mediaVariantMutations.d.ts.map +1 -0
- package/dist/component/mediaVariantMutations.js +737 -0
- package/dist/component/mediaVariantMutations.js.map +1 -0
- package/dist/component/mediaVariants.d.ts +525 -0
- package/dist/component/mediaVariants.d.ts.map +1 -0
- package/dist/component/mediaVariants.js +661 -0
- package/dist/component/mediaVariants.js.map +1 -0
- package/dist/component/ragContentIndexer.d.ts +595 -0
- package/dist/component/ragContentIndexer.d.ts.map +1 -0
- package/dist/component/ragContentIndexer.js +794 -0
- package/dist/component/ragContentIndexer.js.map +1 -0
- package/dist/component/rateLimitHooks.d.ts +266 -0
- package/dist/component/rateLimitHooks.d.ts.map +1 -0
- package/dist/component/rateLimitHooks.js +412 -0
- package/dist/component/rateLimitHooks.js.map +1 -0
- package/dist/component/roles.d.ts +649 -0
- package/dist/component/roles.d.ts.map +1 -0
- package/dist/component/roles.js +884 -0
- package/dist/component/roles.js.map +1 -0
- package/dist/component/scheduledPublish.d.ts +182 -0
- package/dist/component/scheduledPublish.d.ts.map +1 -0
- package/dist/component/scheduledPublish.js +304 -0
- package/dist/component/scheduledPublish.js.map +1 -0
- package/dist/component/schema.d.ts +4114 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +469 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/taxonomies.d.ts +476 -0
- package/dist/component/taxonomies.d.ts.map +1 -0
- package/dist/component/taxonomies.js +785 -0
- package/dist/component/taxonomies.js.map +1 -0
- package/dist/component/taxonomyMutations.d.ts +206 -0
- package/dist/component/taxonomyMutations.d.ts.map +1 -0
- package/dist/component/taxonomyMutations.js +1001 -0
- package/dist/component/taxonomyMutations.js.map +1 -0
- package/dist/component/trash.d.ts +265 -0
- package/dist/component/trash.d.ts.map +1 -0
- package/dist/component/trash.js +621 -0
- package/dist/component/trash.js.map +1 -0
- package/dist/component/types.d.ts +4 -0
- package/dist/component/types.d.ts.map +1 -0
- package/dist/component/types.js +2 -0
- package/dist/component/types.js.map +1 -0
- package/dist/component/userContext.d.ts +508 -0
- package/dist/component/userContext.d.ts.map +1 -0
- package/dist/component/userContext.js +615 -0
- package/dist/component/userContext.js.map +1 -0
- package/dist/component/validation.d.ts +387 -0
- package/dist/component/validation.d.ts.map +1 -0
- package/dist/component/validation.js +1052 -0
- package/dist/component/validation.js.map +1 -0
- package/dist/component/validators.d.ts +4645 -0
- package/dist/component/validators.d.ts.map +1 -0
- package/dist/component/validators.js +641 -0
- package/dist/component/validators.js.map +1 -0
- package/dist/component/versionMutations.d.ts +216 -0
- package/dist/component/versionMutations.d.ts.map +1 -0
- package/dist/component/versionMutations.js +321 -0
- package/dist/component/versionMutations.js.map +1 -0
- package/dist/component/webhookTrigger.d.ts +770 -0
- package/dist/component/webhookTrigger.d.ts.map +1 -0
- package/dist/component/webhookTrigger.js +1413 -0
- package/dist/component/webhookTrigger.js.map +1 -0
- package/dist/react/index.d.ts +316 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +558 -0
- package/dist/react/index.js.map +1 -0
- package/dist/test.d.ts +2230 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +1107 -0
- package/dist/test.js.map +1 -0
- package/package.json +95 -0
- package/src/cli/commands/admin.ts +104 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/utils/detectConvexUrl.ts +54 -0
- package/src/cli/utils/openBrowser.ts +16 -0
- package/src/client/admin-config.ts +138 -0
- package/src/client/adminApi.ts +942 -0
- package/src/client/agentTools.ts +1311 -0
- package/src/client/argTypes.ts +316 -0
- package/src/client/field-types.ts +187 -0
- package/src/client/index.ts +1301 -0
- package/src/client/queryBuilder.ts +1100 -0
- package/src/client/schema/codegen.ts +500 -0
- package/src/client/schema/defineContentType.ts +501 -0
- package/src/client/schema/index.ts +169 -0
- package/src/client/schema/schemaDrift.ts +574 -0
- package/src/client/schema/typedClient.ts +688 -0
- package/src/client/schema/types.ts +666 -0
- package/src/client/types.ts +723 -0
- package/src/client/workflows.ts +141 -0
- package/src/client/wrapper.ts +4304 -0
- package/src/component/_generated/api.ts +140 -0
- package/src/component/_generated/component.ts +5029 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/authorization.ts +647 -0
- package/src/component/authorizationHooks.ts +668 -0
- package/src/component/bulkOperations.ts +687 -0
- package/src/component/contentEntries.ts +1976 -0
- package/src/component/contentEntryMutations.ts +1223 -0
- package/src/component/contentEntryValidation.ts +707 -0
- package/src/component/contentLock.ts +550 -0
- package/src/component/contentTypeMigration.ts +1064 -0
- package/src/component/contentTypeMutations.ts +969 -0
- package/src/component/contentTypes.ts +346 -0
- package/src/component/convex.config.ts +44 -0
- package/src/component/documentTypes.ts +240 -0
- package/src/component/eventEmitter.ts +485 -0
- package/src/component/exportImport.ts +1169 -0
- package/src/component/index.ts +491 -0
- package/src/component/lib/deepReferenceResolver.ts +999 -0
- package/src/component/lib/errors.ts +816 -0
- package/src/component/lib/index.ts +145 -0
- package/src/component/lib/mediaReferenceResolver.ts +495 -0
- package/src/component/lib/metadataExtractor.ts +792 -0
- package/src/component/lib/mutationAuth.ts +199 -0
- package/src/component/lib/queries.ts +79 -0
- package/src/component/lib/ragContentChunker.ts +1371 -0
- package/src/component/lib/referenceResolver.ts +430 -0
- package/src/component/lib/slugGenerator.ts +262 -0
- package/src/component/lib/slugUniqueness.ts +333 -0
- package/src/component/lib/softDelete.ts +44 -0
- package/src/component/localeFallbackChain.ts +673 -0
- package/src/component/localeFields.ts +896 -0
- package/src/component/mediaAssetMutations.ts +725 -0
- package/src/component/mediaAssets.ts +932 -0
- package/src/component/mediaFolderMutations.ts +1046 -0
- package/src/component/mediaUploadMutations.ts +224 -0
- package/src/component/mediaVariantMutations.ts +900 -0
- package/src/component/mediaVariants.ts +793 -0
- package/src/component/ragContentIndexer.ts +1067 -0
- package/src/component/rateLimitHooks.ts +572 -0
- package/src/component/roles.ts +1360 -0
- package/src/component/scheduledPublish.ts +358 -0
- package/src/component/schema.ts +617 -0
- package/src/component/taxonomies.ts +949 -0
- package/src/component/taxonomyMutations.ts +1210 -0
- package/src/component/trash.ts +724 -0
- package/src/component/userContext.ts +898 -0
- package/src/component/validation.ts +1388 -0
- package/src/component/validators.ts +949 -0
- package/src/component/versionMutations.ts +392 -0
- package/src/component/webhookTrigger.ts +1922 -0
- package/src/react/index.ts +898 -0
- package/src/test.ts +1580 -0
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @convex-cms/core/react
|
|
3
|
+
*
|
|
4
|
+
* React hooks and utilities for using Convex CMS in React applications.
|
|
5
|
+
* These hooks provide convenient wrappers around Convex's React hooks
|
|
6
|
+
* specifically designed for CMS use cases.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* import { useContentEntries, useContentEntry, useMediaAssets } from "@convex-cms/core/react";
|
|
11
|
+
* import { api } from "../convex/_generated/api";
|
|
12
|
+
*
|
|
13
|
+
* function BlogList() {
|
|
14
|
+
* const { entries, isLoading, loadMore, hasMore } = useContentEntries(
|
|
15
|
+
* api.example.listBlogPosts,
|
|
16
|
+
* { status: "published" }
|
|
17
|
+
* );
|
|
18
|
+
*
|
|
19
|
+
* return (
|
|
20
|
+
* <div>
|
|
21
|
+
* {entries.map(entry => <BlogCard key={entry._id} entry={entry} />)}
|
|
22
|
+
* {hasMore && <button onClick={loadMore}>Load More</button>}
|
|
23
|
+
* </div>
|
|
24
|
+
* );
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// Re-export core Convex React hooks for convenience
|
|
30
|
+
export {
|
|
31
|
+
useQuery,
|
|
32
|
+
useMutation,
|
|
33
|
+
useAction,
|
|
34
|
+
usePaginatedQuery,
|
|
35
|
+
useConvex,
|
|
36
|
+
useConvexAuth,
|
|
37
|
+
Authenticated,
|
|
38
|
+
Unauthenticated,
|
|
39
|
+
AuthLoading,
|
|
40
|
+
} from "convex/react";
|
|
41
|
+
|
|
42
|
+
// Re-export Convex React provider
|
|
43
|
+
export { ConvexProvider, ConvexProviderWithAuth } from "convex/react";
|
|
44
|
+
|
|
45
|
+
import { useMemo, useCallback, useState, useRef, useReducer, useEffect } from "react";
|
|
46
|
+
import { useQuery, usePaginatedQuery, useMutation } from "convex/react";
|
|
47
|
+
import type { FunctionReference, FunctionArgs, FunctionReturnType } from "convex/server";
|
|
48
|
+
import type { PaginationResult } from "convex/server";
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Upload Utilities
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
function generateUploadId(): string {
|
|
55
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getImageDimensions(file: File, timeoutMs = 5000): Promise<{ width: number; height: number } | undefined> {
|
|
59
|
+
if (!file.type.startsWith("image/")) return Promise.resolve(undefined);
|
|
60
|
+
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
const img = new Image();
|
|
63
|
+
let objectUrl: string | null = null;
|
|
64
|
+
|
|
65
|
+
const timeoutId = setTimeout(() => {
|
|
66
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
67
|
+
resolve(undefined);
|
|
68
|
+
}, timeoutMs);
|
|
69
|
+
|
|
70
|
+
img.onload = () => {
|
|
71
|
+
clearTimeout(timeoutId);
|
|
72
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
73
|
+
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
img.onerror = () => {
|
|
77
|
+
clearTimeout(timeoutId);
|
|
78
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
79
|
+
resolve(undefined);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
objectUrl = URL.createObjectURL(file);
|
|
83
|
+
img.src = objectUrl;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function uploadWithXHR(
|
|
88
|
+
url: string,
|
|
89
|
+
file: File,
|
|
90
|
+
signal: AbortSignal,
|
|
91
|
+
onProgress: (progress: number) => void
|
|
92
|
+
): Promise<string> {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const xhr = new XMLHttpRequest();
|
|
95
|
+
|
|
96
|
+
const abortHandler = () => {
|
|
97
|
+
xhr.abort();
|
|
98
|
+
reject(new DOMException("Upload aborted", "AbortError"));
|
|
99
|
+
};
|
|
100
|
+
signal.addEventListener("abort", abortHandler);
|
|
101
|
+
|
|
102
|
+
xhr.upload.onprogress = (event) => {
|
|
103
|
+
if (event.lengthComputable) {
|
|
104
|
+
const percent = Math.round((event.loaded / event.total) * 80);
|
|
105
|
+
onProgress(10 + percent);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
xhr.onload = () => {
|
|
110
|
+
signal.removeEventListener("abort", abortHandler);
|
|
111
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
112
|
+
try {
|
|
113
|
+
const response = JSON.parse(xhr.responseText);
|
|
114
|
+
resolve(response.storageId);
|
|
115
|
+
} catch {
|
|
116
|
+
reject(new Error("Invalid response from upload server"));
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
reject(new Error(`Upload failed: ${xhr.statusText || `HTTP ${xhr.status}`}`));
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
xhr.onerror = () => {
|
|
124
|
+
signal.removeEventListener("abort", abortHandler);
|
|
125
|
+
reject(new Error("Network error during upload"));
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
xhr.ontimeout = () => {
|
|
129
|
+
signal.removeEventListener("abort", abortHandler);
|
|
130
|
+
reject(new Error("Upload timed out"));
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
xhr.open("POST", url);
|
|
134
|
+
xhr.setRequestHeader("Content-Type", file.type);
|
|
135
|
+
xhr.send(file);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// Types
|
|
141
|
+
// =============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Result type for useContentEntries hook
|
|
145
|
+
*/
|
|
146
|
+
export interface UseContentEntriesResult<T> {
|
|
147
|
+
/** Array of content entries */
|
|
148
|
+
entries: T[];
|
|
149
|
+
/** Whether the initial load is in progress */
|
|
150
|
+
isLoading: boolean;
|
|
151
|
+
/** Whether more entries are being loaded */
|
|
152
|
+
isLoadingMore: boolean;
|
|
153
|
+
/** Load more entries (call when user scrolls/clicks load more) */
|
|
154
|
+
loadMore: (numItems?: number) => void;
|
|
155
|
+
/** Whether there are more entries to load */
|
|
156
|
+
hasMore: boolean;
|
|
157
|
+
/** The current pagination status */
|
|
158
|
+
status: "LoadingFirstPage" | "CanLoadMore" | "LoadingMore" | "Exhausted";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Result type for useContentEntry hook
|
|
163
|
+
*/
|
|
164
|
+
export interface UseContentEntryResult<T> {
|
|
165
|
+
/** The content entry, or undefined if loading/not found */
|
|
166
|
+
entry: T | undefined;
|
|
167
|
+
/** Whether the entry is loading */
|
|
168
|
+
isLoading: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Result type for useMediaAssets hook
|
|
173
|
+
*/
|
|
174
|
+
export interface UseMediaAssetsResult<T> {
|
|
175
|
+
/** Array of media assets */
|
|
176
|
+
assets: T[];
|
|
177
|
+
/** Whether the initial load is in progress */
|
|
178
|
+
isLoading: boolean;
|
|
179
|
+
/** Whether more assets are being loaded */
|
|
180
|
+
isLoadingMore: boolean;
|
|
181
|
+
/** Load more assets */
|
|
182
|
+
loadMore: (numItems?: number) => void;
|
|
183
|
+
/** Whether there are more assets to load */
|
|
184
|
+
hasMore: boolean;
|
|
185
|
+
/** The current pagination status */
|
|
186
|
+
status: "LoadingFirstPage" | "CanLoadMore" | "LoadingMore" | "Exhausted";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Options for CMS hooks
|
|
191
|
+
*/
|
|
192
|
+
export interface CmsHookOptions {
|
|
193
|
+
/** Number of items to load per page */
|
|
194
|
+
pageSize?: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// =============================================================================
|
|
198
|
+
// Content Entry Hooks
|
|
199
|
+
// =============================================================================
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Hook for fetching paginated content entries with automatic cursor management.
|
|
203
|
+
*
|
|
204
|
+
* @param queryFn - The Convex query function for listing entries
|
|
205
|
+
* @param args - Arguments to pass to the query (excluding pagination)
|
|
206
|
+
* @param options - Hook options like page size
|
|
207
|
+
* @returns Paginated entries with loading state and load more function
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```tsx
|
|
211
|
+
* const { entries, isLoading, loadMore, hasMore } = useContentEntries(
|
|
212
|
+
* api.example.listBlogPosts,
|
|
213
|
+
* { contentTypeId: blogTypeId, status: "published" },
|
|
214
|
+
* { pageSize: 10 }
|
|
215
|
+
* );
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
export function useContentEntries<
|
|
219
|
+
Query extends FunctionReference<"query">,
|
|
220
|
+
Args extends FunctionArgs<Query>,
|
|
221
|
+
Result extends FunctionReturnType<Query>
|
|
222
|
+
>(
|
|
223
|
+
queryFn: Query,
|
|
224
|
+
args: Omit<Args, "paginationOpts">,
|
|
225
|
+
options: CmsHookOptions = {}
|
|
226
|
+
): UseContentEntriesResult<Result extends PaginationResult<infer T> ? T : Result extends { page: (infer T)[] } ? T : unknown> {
|
|
227
|
+
const { pageSize = 20 } = options;
|
|
228
|
+
|
|
229
|
+
const result = usePaginatedQuery(
|
|
230
|
+
queryFn,
|
|
231
|
+
args as Args,
|
|
232
|
+
{ initialNumItems: pageSize }
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const entries = useMemo(() => {
|
|
236
|
+
if (!result.results) return [];
|
|
237
|
+
return result.results as (Result extends PaginationResult<infer T> ? T : Result extends { page: (infer T)[] } ? T : unknown)[];
|
|
238
|
+
}, [result.results]);
|
|
239
|
+
|
|
240
|
+
const loadMore = useCallback(
|
|
241
|
+
(numItems?: number) => {
|
|
242
|
+
result.loadMore(numItems ?? pageSize);
|
|
243
|
+
},
|
|
244
|
+
[result, pageSize]
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
entries,
|
|
249
|
+
isLoading: result.status === "LoadingFirstPage",
|
|
250
|
+
isLoadingMore: result.status === "LoadingMore",
|
|
251
|
+
loadMore,
|
|
252
|
+
hasMore: result.status === "CanLoadMore",
|
|
253
|
+
status: result.status,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Hook for fetching a single content entry.
|
|
259
|
+
*
|
|
260
|
+
* @param queryFn - The Convex query function for getting an entry
|
|
261
|
+
* @param args - Arguments to pass to the query (typically entry ID)
|
|
262
|
+
* @returns The entry with loading state
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```tsx
|
|
266
|
+
* const { entry, isLoading } = useContentEntry(
|
|
267
|
+
* api.example.getBlogPost,
|
|
268
|
+
* { id: postId, locale: "en-US" }
|
|
269
|
+
* );
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
export function useContentEntry<
|
|
273
|
+
Query extends FunctionReference<"query">,
|
|
274
|
+
Args extends FunctionArgs<Query>,
|
|
275
|
+
Result extends FunctionReturnType<Query>
|
|
276
|
+
>(
|
|
277
|
+
queryFn: Query,
|
|
278
|
+
args: Args
|
|
279
|
+
): UseContentEntryResult<Result> {
|
|
280
|
+
const result = useQuery(queryFn, args);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
entry: result as Result | undefined,
|
|
284
|
+
isLoading: result === undefined,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// =============================================================================
|
|
289
|
+
// Media Asset Hooks
|
|
290
|
+
// =============================================================================
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Hook for fetching paginated media assets.
|
|
294
|
+
*
|
|
295
|
+
* @param queryFn - The Convex query function for listing media
|
|
296
|
+
* @param args - Arguments to pass to the query (folder, type filters, etc.)
|
|
297
|
+
* @param options - Hook options like page size
|
|
298
|
+
* @returns Paginated assets with loading state and load more function
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```tsx
|
|
302
|
+
* const { assets, isLoading, loadMore, hasMore } = useMediaAssets(
|
|
303
|
+
* api.example.listMedia,
|
|
304
|
+
* { folderId: currentFolder, type: "image" },
|
|
305
|
+
* { pageSize: 24 }
|
|
306
|
+
* );
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
export function useMediaAssets<
|
|
310
|
+
Query extends FunctionReference<"query">,
|
|
311
|
+
Args extends FunctionArgs<Query>,
|
|
312
|
+
Result extends FunctionReturnType<Query>
|
|
313
|
+
>(
|
|
314
|
+
queryFn: Query,
|
|
315
|
+
args: Omit<Args, "paginationOpts">,
|
|
316
|
+
options: CmsHookOptions = {}
|
|
317
|
+
): UseMediaAssetsResult<Result extends PaginationResult<infer T> ? T : Result extends { page: (infer T)[] } ? T : unknown> {
|
|
318
|
+
const { pageSize = 24 } = options;
|
|
319
|
+
|
|
320
|
+
const result = usePaginatedQuery(
|
|
321
|
+
queryFn,
|
|
322
|
+
args as Args,
|
|
323
|
+
{ initialNumItems: pageSize }
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const assets = useMemo(() => {
|
|
327
|
+
if (!result.results) return [];
|
|
328
|
+
return result.results as (Result extends PaginationResult<infer T> ? T : Result extends { page: (infer T)[] } ? T : unknown)[];
|
|
329
|
+
}, [result.results]);
|
|
330
|
+
|
|
331
|
+
const loadMore = useCallback(
|
|
332
|
+
(numItems?: number) => {
|
|
333
|
+
result.loadMore(numItems ?? pageSize);
|
|
334
|
+
},
|
|
335
|
+
[result, pageSize]
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
assets,
|
|
340
|
+
isLoading: result.status === "LoadingFirstPage",
|
|
341
|
+
isLoadingMore: result.status === "LoadingMore",
|
|
342
|
+
loadMore,
|
|
343
|
+
hasMore: result.status === "CanLoadMore",
|
|
344
|
+
status: result.status,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// =============================================================================
|
|
349
|
+
// Mutation Hooks
|
|
350
|
+
// =============================================================================
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Result type for useCmsMutation hook
|
|
354
|
+
*/
|
|
355
|
+
export interface UseCmsMutationResult<Args, Result> {
|
|
356
|
+
/** Execute the mutation */
|
|
357
|
+
mutate: (args: Args) => Promise<Result>;
|
|
358
|
+
/** Whether the mutation is in progress */
|
|
359
|
+
isPending: boolean;
|
|
360
|
+
/** The last error that occurred */
|
|
361
|
+
error: Error | null;
|
|
362
|
+
/** Reset the error state */
|
|
363
|
+
resetError: () => void;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Hook for CMS mutations with loading and error state tracking.
|
|
368
|
+
*
|
|
369
|
+
* @param mutationFn - The Convex mutation function
|
|
370
|
+
* @returns Mutation function with state tracking
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```tsx
|
|
374
|
+
* const { mutate: createEntry, isPending, error } = useCmsMutation(
|
|
375
|
+
* api.example.createBlogPost
|
|
376
|
+
* );
|
|
377
|
+
*
|
|
378
|
+
* const handleSubmit = async (data) => {
|
|
379
|
+
* try {
|
|
380
|
+
* await createEntry(data);
|
|
381
|
+
* toast.success("Post created!");
|
|
382
|
+
* } catch (e) {
|
|
383
|
+
* // Error is also available via the error state
|
|
384
|
+
* }
|
|
385
|
+
* };
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
export function useCmsMutation<
|
|
389
|
+
Mutation extends FunctionReference<"mutation">,
|
|
390
|
+
Args extends FunctionArgs<Mutation>,
|
|
391
|
+
Result extends FunctionReturnType<Mutation>
|
|
392
|
+
>(
|
|
393
|
+
mutationFn: Mutation
|
|
394
|
+
): UseCmsMutationResult<Args, Awaited<Result>> {
|
|
395
|
+
const mutation = useMutation(mutationFn);
|
|
396
|
+
const [isPending, setIsPending] = useState(false);
|
|
397
|
+
const [error, setError] = useState<Error | null>(null);
|
|
398
|
+
|
|
399
|
+
const mutate = useCallback(
|
|
400
|
+
async (args: Args): Promise<Awaited<Result>> => {
|
|
401
|
+
setIsPending(true);
|
|
402
|
+
setError(null);
|
|
403
|
+
try {
|
|
404
|
+
const result = await mutation(args);
|
|
405
|
+
return result as Awaited<Result>;
|
|
406
|
+
} catch (e) {
|
|
407
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
408
|
+
setError(err);
|
|
409
|
+
throw err;
|
|
410
|
+
} finally {
|
|
411
|
+
setIsPending(false);
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
[mutation]
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const resetError = useCallback(() => {
|
|
418
|
+
setError(null);
|
|
419
|
+
}, []);
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
mutate,
|
|
423
|
+
isPending,
|
|
424
|
+
error,
|
|
425
|
+
resetError,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// =============================================================================
|
|
430
|
+
// Utility Hooks
|
|
431
|
+
// =============================================================================
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Result type for useMediaUpload hook
|
|
435
|
+
*/
|
|
436
|
+
export interface UseMediaUploadResult<Result> {
|
|
437
|
+
/** Upload a file with optional metadata */
|
|
438
|
+
upload: (file: File, metadata?: Record<string, unknown>) => Promise<Result>;
|
|
439
|
+
/** Cancel the current upload */
|
|
440
|
+
cancel: () => void;
|
|
441
|
+
/** Whether an upload is in progress */
|
|
442
|
+
isUploading: boolean;
|
|
443
|
+
/** Upload progress (0-100) */
|
|
444
|
+
progress: number;
|
|
445
|
+
/** Last error message, null if no error */
|
|
446
|
+
error: string | null;
|
|
447
|
+
/** Reset the upload state */
|
|
448
|
+
reset: () => void;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Hook for uploading files to Convex storage with CMS media asset creation.
|
|
453
|
+
* Includes real-time progress tracking, cancellation support, and error handling.
|
|
454
|
+
*
|
|
455
|
+
* @param getUploadUrl - Mutation to get a storage upload URL
|
|
456
|
+
* @param createAsset - Mutation to create the media asset record
|
|
457
|
+
* @returns Upload function with progress tracking, cancellation, and error state
|
|
458
|
+
*
|
|
459
|
+
* @example
|
|
460
|
+
* ```tsx
|
|
461
|
+
* const { upload, cancel, isUploading, progress, error } = useMediaUpload(
|
|
462
|
+
* api.example.generateUploadUrl,
|
|
463
|
+
* api.example.createMediaAsset
|
|
464
|
+
* );
|
|
465
|
+
*
|
|
466
|
+
* const handleUpload = async (file: File) => {
|
|
467
|
+
* try {
|
|
468
|
+
* const asset = await upload(file, { parentId: folderId });
|
|
469
|
+
* console.log("Uploaded:", asset);
|
|
470
|
+
* } catch (e) {
|
|
471
|
+
* if (e.name !== "AbortError") {
|
|
472
|
+
* console.error("Upload failed:", e);
|
|
473
|
+
* }
|
|
474
|
+
* }
|
|
475
|
+
* };
|
|
476
|
+
*
|
|
477
|
+
* // Cancel button
|
|
478
|
+
* <button onClick={cancel} disabled={!isUploading}>Cancel</button>
|
|
479
|
+
* ```
|
|
480
|
+
*/
|
|
481
|
+
export function useMediaUpload<
|
|
482
|
+
UploadMutation extends FunctionReference<"mutation">,
|
|
483
|
+
CreateMutation extends FunctionReference<"mutation">,
|
|
484
|
+
CreateArgs extends FunctionArgs<CreateMutation>
|
|
485
|
+
>(
|
|
486
|
+
getUploadUrl: UploadMutation,
|
|
487
|
+
createAsset: CreateMutation
|
|
488
|
+
): UseMediaUploadResult<FunctionReturnType<CreateMutation>> {
|
|
489
|
+
const generateUrl = useMutation(getUploadUrl);
|
|
490
|
+
const create = useMutation(createAsset);
|
|
491
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
492
|
+
const [progress, setProgress] = useState(0);
|
|
493
|
+
const [error, setError] = useState<string | null>(null);
|
|
494
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
495
|
+
|
|
496
|
+
const upload = useCallback(
|
|
497
|
+
async (file: File, metadata?: Record<string, unknown>): Promise<FunctionReturnType<CreateMutation>> => {
|
|
498
|
+
abortControllerRef.current = new AbortController();
|
|
499
|
+
setIsUploading(true);
|
|
500
|
+
setProgress(0);
|
|
501
|
+
setError(null);
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const uploadUrl = await generateUrl({});
|
|
505
|
+
setProgress(5);
|
|
506
|
+
|
|
507
|
+
const storageId = await uploadWithXHR(
|
|
508
|
+
uploadUrl as string,
|
|
509
|
+
file,
|
|
510
|
+
abortControllerRef.current.signal,
|
|
511
|
+
setProgress
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
setProgress(90);
|
|
515
|
+
|
|
516
|
+
const dimensions = await getImageDimensions(file);
|
|
517
|
+
|
|
518
|
+
const asset = await create({
|
|
519
|
+
storageId,
|
|
520
|
+
name: file.name,
|
|
521
|
+
mimeType: file.type,
|
|
522
|
+
size: file.size,
|
|
523
|
+
...dimensions,
|
|
524
|
+
...metadata,
|
|
525
|
+
} as CreateArgs);
|
|
526
|
+
|
|
527
|
+
setProgress(100);
|
|
528
|
+
return asset;
|
|
529
|
+
} catch (e) {
|
|
530
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
531
|
+
if (err.name !== "AbortError") {
|
|
532
|
+
setError(err.message);
|
|
533
|
+
}
|
|
534
|
+
throw err;
|
|
535
|
+
} finally {
|
|
536
|
+
setIsUploading(false);
|
|
537
|
+
abortControllerRef.current = null;
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
[generateUrl, create]
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const cancel = useCallback(() => {
|
|
544
|
+
abortControllerRef.current?.abort();
|
|
545
|
+
}, []);
|
|
546
|
+
|
|
547
|
+
const reset = useCallback(() => {
|
|
548
|
+
setProgress(0);
|
|
549
|
+
setError(null);
|
|
550
|
+
setIsUploading(false);
|
|
551
|
+
}, []);
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
upload,
|
|
555
|
+
cancel,
|
|
556
|
+
isUploading,
|
|
557
|
+
progress,
|
|
558
|
+
error,
|
|
559
|
+
reset,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// =============================================================================
|
|
564
|
+
// Multi-file Upload Queue
|
|
565
|
+
// =============================================================================
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Status of a file in the upload queue
|
|
569
|
+
*/
|
|
570
|
+
export type UploadQueueFileStatus = "pending" | "uploading" | "complete" | "error" | "cancelled";
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* A file in the upload queue
|
|
574
|
+
*/
|
|
575
|
+
export interface UploadQueueFile {
|
|
576
|
+
/** Unique ID for this upload */
|
|
577
|
+
id: string;
|
|
578
|
+
/** The file being uploaded */
|
|
579
|
+
file: File;
|
|
580
|
+
/** Current status */
|
|
581
|
+
status: UploadQueueFileStatus;
|
|
582
|
+
/** Upload progress (0-100) */
|
|
583
|
+
progress: number;
|
|
584
|
+
/** Error message if status is 'error' */
|
|
585
|
+
error?: string;
|
|
586
|
+
/** Result from createAsset if status is 'complete' */
|
|
587
|
+
result?: unknown;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Options for useMediaUploadQueue hook
|
|
592
|
+
*/
|
|
593
|
+
export interface UseMediaUploadQueueOptions<
|
|
594
|
+
UploadMutation extends FunctionReference<"mutation">,
|
|
595
|
+
CreateMutation extends FunctionReference<"mutation">
|
|
596
|
+
> {
|
|
597
|
+
/** Mutation to get a storage upload URL */
|
|
598
|
+
getUploadUrl: UploadMutation;
|
|
599
|
+
/** Mutation to create the media asset record */
|
|
600
|
+
createAsset: CreateMutation;
|
|
601
|
+
/** Maximum concurrent uploads (default: 3) */
|
|
602
|
+
maxConcurrent?: number;
|
|
603
|
+
/** Metadata to include with each uploaded asset */
|
|
604
|
+
metadata?: Record<string, unknown>;
|
|
605
|
+
/** Called when all uploads complete */
|
|
606
|
+
onComplete?: (results: UploadQueueFile[]) => void;
|
|
607
|
+
/** Called when a file upload fails */
|
|
608
|
+
onError?: (file: UploadQueueFile) => void;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Result type for useMediaUploadQueue hook
|
|
613
|
+
*/
|
|
614
|
+
export interface UseMediaUploadQueueResult {
|
|
615
|
+
/** Current files in the queue */
|
|
616
|
+
files: UploadQueueFile[];
|
|
617
|
+
/** Add files to the queue (starts uploading automatically) */
|
|
618
|
+
addFiles: (files: File[]) => void;
|
|
619
|
+
/** Cancel a specific file upload */
|
|
620
|
+
cancelFile: (id: string) => void;
|
|
621
|
+
/** Cancel all pending/uploading files */
|
|
622
|
+
cancelAll: () => void;
|
|
623
|
+
/** Retry a failed upload */
|
|
624
|
+
retryFile: (id: string) => void;
|
|
625
|
+
/** Remove completed/failed files from the queue */
|
|
626
|
+
clearCompleted: () => void;
|
|
627
|
+
/** Clear the entire queue */
|
|
628
|
+
clearAll: () => void;
|
|
629
|
+
/** Whether any uploads are in progress */
|
|
630
|
+
isUploading: boolean;
|
|
631
|
+
/** Overall progress (0-100) */
|
|
632
|
+
overallProgress: number;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
type QueueAction =
|
|
636
|
+
| { type: "ADD_FILES"; files: File[] }
|
|
637
|
+
| { type: "UPDATE_FILE"; id: string; updates: Partial<UploadQueueFile> }
|
|
638
|
+
| { type: "RETRY_FILE"; id: string }
|
|
639
|
+
| { type: "REMOVE_FILE"; id: string }
|
|
640
|
+
| { type: "CLEAR_COMPLETED" }
|
|
641
|
+
| { type: "CLEAR_ALL" }
|
|
642
|
+
| { type: "CANCEL_FILE"; id: string }
|
|
643
|
+
| { type: "CANCEL_ALL" };
|
|
644
|
+
|
|
645
|
+
function queueReducer(state: UploadQueueFile[], action: QueueAction): UploadQueueFile[] {
|
|
646
|
+
switch (action.type) {
|
|
647
|
+
case "ADD_FILES": {
|
|
648
|
+
const newFiles: UploadQueueFile[] = action.files.map((file) => ({
|
|
649
|
+
id: generateUploadId(),
|
|
650
|
+
file,
|
|
651
|
+
status: "pending",
|
|
652
|
+
progress: 0,
|
|
653
|
+
}));
|
|
654
|
+
return [...state, ...newFiles];
|
|
655
|
+
}
|
|
656
|
+
case "UPDATE_FILE":
|
|
657
|
+
return state.map((f) =>
|
|
658
|
+
f.id === action.id ? { ...f, ...action.updates } : f
|
|
659
|
+
);
|
|
660
|
+
case "RETRY_FILE":
|
|
661
|
+
return state.map((f) =>
|
|
662
|
+
f.id === action.id
|
|
663
|
+
? { ...f, status: "pending", progress: 0, error: undefined }
|
|
664
|
+
: f
|
|
665
|
+
);
|
|
666
|
+
case "REMOVE_FILE":
|
|
667
|
+
return state.filter((f) => f.id !== action.id);
|
|
668
|
+
case "CLEAR_COMPLETED":
|
|
669
|
+
return state.filter(
|
|
670
|
+
(f) => f.status === "pending" || f.status === "uploading"
|
|
671
|
+
);
|
|
672
|
+
case "CLEAR_ALL":
|
|
673
|
+
return [];
|
|
674
|
+
case "CANCEL_FILE":
|
|
675
|
+
return state.map((f) =>
|
|
676
|
+
f.id === action.id && (f.status === "pending" || f.status === "uploading")
|
|
677
|
+
? { ...f, status: "cancelled", error: "Upload cancelled" }
|
|
678
|
+
: f
|
|
679
|
+
);
|
|
680
|
+
case "CANCEL_ALL":
|
|
681
|
+
return state.map((f) =>
|
|
682
|
+
f.status === "pending" || f.status === "uploading"
|
|
683
|
+
? { ...f, status: "cancelled", error: "Upload cancelled" }
|
|
684
|
+
: f
|
|
685
|
+
);
|
|
686
|
+
default:
|
|
687
|
+
return state;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Hook for uploading multiple files with queue management, concurrency control,
|
|
693
|
+
* and progress tracking. Uses a reducer for state management to avoid closure issues.
|
|
694
|
+
*
|
|
695
|
+
* @param options - Configuration options
|
|
696
|
+
* @returns Queue state and control functions
|
|
697
|
+
*
|
|
698
|
+
* @example
|
|
699
|
+
* ```tsx
|
|
700
|
+
* const queue = useMediaUploadQueue({
|
|
701
|
+
* getUploadUrl: api.media.generateUploadUrl,
|
|
702
|
+
* createAsset: api.media.createAsset,
|
|
703
|
+
* maxConcurrent: 3,
|
|
704
|
+
* metadata: { parentId: folderId },
|
|
705
|
+
* onComplete: (results) => console.log("All done!", results),
|
|
706
|
+
* });
|
|
707
|
+
*
|
|
708
|
+
* // In your dropzone handler:
|
|
709
|
+
* const handleDrop = (files: File[]) => {
|
|
710
|
+
* queue.addFiles(files);
|
|
711
|
+
* };
|
|
712
|
+
*
|
|
713
|
+
* // Display progress:
|
|
714
|
+
* {queue.files.map(f => (
|
|
715
|
+
* <div key={f.id}>
|
|
716
|
+
* {f.file.name}: {f.status} ({f.progress}%)
|
|
717
|
+
* {f.status === "uploading" && <button onClick={() => queue.cancelFile(f.id)}>Cancel</button>}
|
|
718
|
+
* {f.status === "error" && <button onClick={() => queue.retryFile(f.id)}>Retry</button>}
|
|
719
|
+
* </div>
|
|
720
|
+
* ))}
|
|
721
|
+
* ```
|
|
722
|
+
*/
|
|
723
|
+
export function useMediaUploadQueue<
|
|
724
|
+
UploadMutation extends FunctionReference<"mutation">,
|
|
725
|
+
CreateMutation extends FunctionReference<"mutation">
|
|
726
|
+
>(
|
|
727
|
+
options: UseMediaUploadQueueOptions<UploadMutation, CreateMutation>
|
|
728
|
+
): UseMediaUploadQueueResult {
|
|
729
|
+
const { maxConcurrent = 3, metadata, onComplete, onError } = options;
|
|
730
|
+
const [files, dispatch] = useReducer(queueReducer, []);
|
|
731
|
+
const generateUrl = useMutation(options.getUploadUrl);
|
|
732
|
+
const create = useMutation(options.createAsset);
|
|
733
|
+
|
|
734
|
+
const activeUploadsRef = useRef(0);
|
|
735
|
+
const abortControllersRef = useRef<Map<string, AbortController>>(new Map());
|
|
736
|
+
const processingRef = useRef(false);
|
|
737
|
+
|
|
738
|
+
const uploadFile = useCallback(
|
|
739
|
+
async (queueFile: UploadQueueFile) => {
|
|
740
|
+
const abortController = new AbortController();
|
|
741
|
+
abortControllersRef.current.set(queueFile.id, abortController);
|
|
742
|
+
|
|
743
|
+
dispatch({
|
|
744
|
+
type: "UPDATE_FILE",
|
|
745
|
+
id: queueFile.id,
|
|
746
|
+
updates: { status: "uploading", progress: 0 },
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
const uploadUrl = await generateUrl({});
|
|
751
|
+
|
|
752
|
+
dispatch({
|
|
753
|
+
type: "UPDATE_FILE",
|
|
754
|
+
id: queueFile.id,
|
|
755
|
+
updates: { progress: 5 },
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const storageId = await uploadWithXHR(
|
|
759
|
+
uploadUrl as string,
|
|
760
|
+
queueFile.file,
|
|
761
|
+
abortController.signal,
|
|
762
|
+
(progress) => {
|
|
763
|
+
dispatch({
|
|
764
|
+
type: "UPDATE_FILE",
|
|
765
|
+
id: queueFile.id,
|
|
766
|
+
updates: { progress },
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
dispatch({
|
|
772
|
+
type: "UPDATE_FILE",
|
|
773
|
+
id: queueFile.id,
|
|
774
|
+
updates: { progress: 90 },
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
const dimensions = await getImageDimensions(queueFile.file);
|
|
778
|
+
|
|
779
|
+
const result = await create({
|
|
780
|
+
storageId,
|
|
781
|
+
name: queueFile.file.name,
|
|
782
|
+
mimeType: queueFile.file.type,
|
|
783
|
+
size: queueFile.file.size,
|
|
784
|
+
...dimensions,
|
|
785
|
+
...metadata,
|
|
786
|
+
} as FunctionArgs<CreateMutation>);
|
|
787
|
+
|
|
788
|
+
dispatch({
|
|
789
|
+
type: "UPDATE_FILE",
|
|
790
|
+
id: queueFile.id,
|
|
791
|
+
updates: { status: "complete", progress: 100, result },
|
|
792
|
+
});
|
|
793
|
+
} catch (e) {
|
|
794
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
795
|
+
if (err.name === "AbortError") {
|
|
796
|
+
dispatch({
|
|
797
|
+
type: "UPDATE_FILE",
|
|
798
|
+
id: queueFile.id,
|
|
799
|
+
updates: { status: "cancelled", error: "Upload cancelled" },
|
|
800
|
+
});
|
|
801
|
+
} else {
|
|
802
|
+
const updatedFile = {
|
|
803
|
+
...queueFile,
|
|
804
|
+
status: "error" as const,
|
|
805
|
+
error: err.message,
|
|
806
|
+
};
|
|
807
|
+
dispatch({
|
|
808
|
+
type: "UPDATE_FILE",
|
|
809
|
+
id: queueFile.id,
|
|
810
|
+
updates: { status: "error", error: err.message },
|
|
811
|
+
});
|
|
812
|
+
onError?.(updatedFile);
|
|
813
|
+
}
|
|
814
|
+
} finally {
|
|
815
|
+
abortControllersRef.current.delete(queueFile.id);
|
|
816
|
+
activeUploadsRef.current--;
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
[generateUrl, create, metadata, onError]
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
const processQueue = useCallback(() => {
|
|
823
|
+
if (processingRef.current) return;
|
|
824
|
+
processingRef.current = true;
|
|
825
|
+
|
|
826
|
+
const pending = files.filter((f) => f.status === "pending");
|
|
827
|
+
|
|
828
|
+
while (activeUploadsRef.current < maxConcurrent && pending.length > 0) {
|
|
829
|
+
const next = pending.shift()!;
|
|
830
|
+
activeUploadsRef.current++;
|
|
831
|
+
uploadFile(next);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
processingRef.current = false;
|
|
835
|
+
|
|
836
|
+
if (
|
|
837
|
+
activeUploadsRef.current === 0 &&
|
|
838
|
+
files.length > 0 &&
|
|
839
|
+
files.every((f) => f.status !== "pending" && f.status !== "uploading")
|
|
840
|
+
) {
|
|
841
|
+
onComplete?.(files);
|
|
842
|
+
}
|
|
843
|
+
}, [files, maxConcurrent, uploadFile, onComplete]);
|
|
844
|
+
|
|
845
|
+
useEffect(() => {
|
|
846
|
+
if (files.some((f) => f.status === "pending") && activeUploadsRef.current < maxConcurrent) {
|
|
847
|
+
processQueue();
|
|
848
|
+
}
|
|
849
|
+
}, [files, maxConcurrent, processQueue]);
|
|
850
|
+
|
|
851
|
+
const addFiles = useCallback((newFiles: File[]) => {
|
|
852
|
+
dispatch({ type: "ADD_FILES", files: newFiles });
|
|
853
|
+
}, []);
|
|
854
|
+
|
|
855
|
+
const cancelFile = useCallback((id: string) => {
|
|
856
|
+
const controller = abortControllersRef.current.get(id);
|
|
857
|
+
if (controller) {
|
|
858
|
+
controller.abort();
|
|
859
|
+
}
|
|
860
|
+
dispatch({ type: "CANCEL_FILE", id });
|
|
861
|
+
}, []);
|
|
862
|
+
|
|
863
|
+
const cancelAll = useCallback(() => {
|
|
864
|
+
abortControllersRef.current.forEach((controller) => controller.abort());
|
|
865
|
+
dispatch({ type: "CANCEL_ALL" });
|
|
866
|
+
}, []);
|
|
867
|
+
|
|
868
|
+
const retryFile = useCallback((id: string) => {
|
|
869
|
+
dispatch({ type: "RETRY_FILE", id });
|
|
870
|
+
}, []);
|
|
871
|
+
|
|
872
|
+
const clearCompleted = useCallback(() => {
|
|
873
|
+
dispatch({ type: "CLEAR_COMPLETED" });
|
|
874
|
+
}, []);
|
|
875
|
+
|
|
876
|
+
const clearAll = useCallback(() => {
|
|
877
|
+
abortControllersRef.current.forEach((controller) => controller.abort());
|
|
878
|
+
dispatch({ type: "CLEAR_ALL" });
|
|
879
|
+
}, []);
|
|
880
|
+
|
|
881
|
+
const isUploading = files.some((f) => f.status === "uploading");
|
|
882
|
+
const overallProgress =
|
|
883
|
+
files.length > 0
|
|
884
|
+
? Math.round(files.reduce((sum, f) => sum + f.progress, 0) / files.length)
|
|
885
|
+
: 0;
|
|
886
|
+
|
|
887
|
+
return {
|
|
888
|
+
files,
|
|
889
|
+
addFiles,
|
|
890
|
+
cancelFile,
|
|
891
|
+
cancelAll,
|
|
892
|
+
retryFile,
|
|
893
|
+
clearCompleted,
|
|
894
|
+
clearAll,
|
|
895
|
+
isUploading,
|
|
896
|
+
overallProgress,
|
|
897
|
+
};
|
|
898
|
+
}
|