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.
Files changed (379) hide show
  1. package/dist/cli/commands/admin.d.ts +16 -0
  2. package/dist/cli/commands/admin.d.ts.map +1 -0
  3. package/dist/cli/commands/admin.js +88 -0
  4. package/dist/cli/commands/admin.js.map +1 -0
  5. package/dist/cli/index.d.ts +3 -0
  6. package/dist/cli/index.d.ts.map +1 -0
  7. package/dist/cli/index.js +18 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/cli/utils/detectConvexUrl.d.ts +13 -0
  10. package/dist/cli/utils/detectConvexUrl.d.ts.map +1 -0
  11. package/dist/cli/utils/detectConvexUrl.js +48 -0
  12. package/dist/cli/utils/detectConvexUrl.js.map +1 -0
  13. package/dist/cli/utils/openBrowser.d.ts +7 -0
  14. package/dist/cli/utils/openBrowser.d.ts.map +1 -0
  15. package/dist/cli/utils/openBrowser.js +17 -0
  16. package/dist/cli/utils/openBrowser.js.map +1 -0
  17. package/dist/client/admin-config.d.ts +126 -0
  18. package/dist/client/admin-config.d.ts.map +1 -0
  19. package/dist/client/admin-config.js +117 -0
  20. package/dist/client/admin-config.js.map +1 -0
  21. package/dist/client/adminApi.d.ts +2273 -0
  22. package/dist/client/adminApi.d.ts.map +1 -0
  23. package/dist/client/adminApi.js +716 -0
  24. package/dist/client/adminApi.js.map +1 -0
  25. package/dist/client/agentTools.d.ts +933 -0
  26. package/dist/client/agentTools.d.ts.map +1 -0
  27. package/dist/client/agentTools.js +1004 -0
  28. package/dist/client/agentTools.js.map +1 -0
  29. package/dist/client/argTypes.d.ts +212 -0
  30. package/dist/client/argTypes.d.ts.map +1 -0
  31. package/dist/client/argTypes.js +5 -0
  32. package/dist/client/argTypes.js.map +1 -0
  33. package/dist/client/field-types.d.ts +55 -0
  34. package/dist/client/field-types.d.ts.map +1 -0
  35. package/dist/client/field-types.js +152 -0
  36. package/dist/client/field-types.js.map +1 -0
  37. package/dist/client/index.d.ts +189 -0
  38. package/dist/client/index.d.ts.map +1 -0
  39. package/dist/client/index.js +668 -0
  40. package/dist/client/index.js.map +1 -0
  41. package/dist/client/queryBuilder.d.ts +765 -0
  42. package/dist/client/queryBuilder.d.ts.map +1 -0
  43. package/dist/client/queryBuilder.js +970 -0
  44. package/dist/client/queryBuilder.js.map +1 -0
  45. package/dist/client/schema/codegen.d.ts +128 -0
  46. package/dist/client/schema/codegen.d.ts.map +1 -0
  47. package/dist/client/schema/codegen.js +318 -0
  48. package/dist/client/schema/codegen.js.map +1 -0
  49. package/dist/client/schema/defineContentType.d.ts +221 -0
  50. package/dist/client/schema/defineContentType.d.ts.map +1 -0
  51. package/dist/client/schema/defineContentType.js +380 -0
  52. package/dist/client/schema/defineContentType.js.map +1 -0
  53. package/dist/client/schema/index.d.ts +85 -0
  54. package/dist/client/schema/index.d.ts.map +1 -0
  55. package/dist/client/schema/index.js +92 -0
  56. package/dist/client/schema/index.js.map +1 -0
  57. package/dist/client/schema/schemaDrift.d.ts +199 -0
  58. package/dist/client/schema/schemaDrift.d.ts.map +1 -0
  59. package/dist/client/schema/schemaDrift.js +340 -0
  60. package/dist/client/schema/schemaDrift.js.map +1 -0
  61. package/dist/client/schema/typedClient.d.ts +401 -0
  62. package/dist/client/schema/typedClient.d.ts.map +1 -0
  63. package/dist/client/schema/typedClient.js +269 -0
  64. package/dist/client/schema/typedClient.js.map +1 -0
  65. package/dist/client/schema/types.d.ts +477 -0
  66. package/dist/client/schema/types.d.ts.map +1 -0
  67. package/dist/client/schema/types.js +39 -0
  68. package/dist/client/schema/types.js.map +1 -0
  69. package/dist/client/types.d.ts +449 -0
  70. package/dist/client/types.d.ts.map +1 -0
  71. package/dist/client/types.js +149 -0
  72. package/dist/client/types.js.map +1 -0
  73. package/dist/client/workflows.d.ts +51 -0
  74. package/dist/client/workflows.d.ts.map +1 -0
  75. package/dist/client/workflows.js +103 -0
  76. package/dist/client/workflows.js.map +1 -0
  77. package/dist/client/wrapper.d.ts +2198 -0
  78. package/dist/client/wrapper.d.ts.map +1 -0
  79. package/dist/client/wrapper.js +2651 -0
  80. package/dist/client/wrapper.js.map +1 -0
  81. package/dist/component/_generated/api.d.ts +124 -0
  82. package/dist/component/_generated/api.d.ts.map +1 -0
  83. package/dist/component/_generated/api.js +31 -0
  84. package/dist/component/_generated/api.js.map +1 -0
  85. package/dist/component/_generated/component.d.ts +4321 -0
  86. package/dist/component/_generated/component.d.ts.map +1 -0
  87. package/dist/component/_generated/component.js +11 -0
  88. package/dist/component/_generated/component.js.map +1 -0
  89. package/dist/component/_generated/dataModel.d.ts +46 -0
  90. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  91. package/dist/component/_generated/dataModel.js +11 -0
  92. package/dist/component/_generated/dataModel.js.map +1 -0
  93. package/dist/component/_generated/server.d.ts +121 -0
  94. package/dist/component/_generated/server.d.ts.map +1 -0
  95. package/dist/component/_generated/server.js +78 -0
  96. package/dist/component/_generated/server.js.map +1 -0
  97. package/dist/component/auditLog.d.ts +410 -0
  98. package/dist/component/auditLog.d.ts.map +1 -0
  99. package/dist/component/auditLog.js +607 -0
  100. package/dist/component/auditLog.js.map +1 -0
  101. package/dist/component/authorization.d.ts +323 -0
  102. package/dist/component/authorization.d.ts.map +1 -0
  103. package/dist/component/authorization.js +464 -0
  104. package/dist/component/authorization.js.map +1 -0
  105. package/dist/component/authorizationHooks.d.ts +184 -0
  106. package/dist/component/authorizationHooks.d.ts.map +1 -0
  107. package/dist/component/authorizationHooks.js +521 -0
  108. package/dist/component/authorizationHooks.js.map +1 -0
  109. package/dist/component/bulkOperations.d.ts +200 -0
  110. package/dist/component/bulkOperations.d.ts.map +1 -0
  111. package/dist/component/bulkOperations.js +568 -0
  112. package/dist/component/bulkOperations.js.map +1 -0
  113. package/dist/component/contentEntries.d.ts +719 -0
  114. package/dist/component/contentEntries.d.ts.map +1 -0
  115. package/dist/component/contentEntries.js +1617 -0
  116. package/dist/component/contentEntries.js.map +1 -0
  117. package/dist/component/contentEntryMutations.d.ts +505 -0
  118. package/dist/component/contentEntryMutations.d.ts.map +1 -0
  119. package/dist/component/contentEntryMutations.js +1009 -0
  120. package/dist/component/contentEntryMutations.js.map +1 -0
  121. package/dist/component/contentEntryValidation.d.ts +115 -0
  122. package/dist/component/contentEntryValidation.d.ts.map +1 -0
  123. package/dist/component/contentEntryValidation.js +546 -0
  124. package/dist/component/contentEntryValidation.js.map +1 -0
  125. package/dist/component/contentLock.d.ts +328 -0
  126. package/dist/component/contentLock.d.ts.map +1 -0
  127. package/dist/component/contentLock.js +471 -0
  128. package/dist/component/contentLock.js.map +1 -0
  129. package/dist/component/contentTypeMigration.d.ts +411 -0
  130. package/dist/component/contentTypeMigration.d.ts.map +1 -0
  131. package/dist/component/contentTypeMigration.js +805 -0
  132. package/dist/component/contentTypeMigration.js.map +1 -0
  133. package/dist/component/contentTypeMutations.d.ts +975 -0
  134. package/dist/component/contentTypeMutations.d.ts.map +1 -0
  135. package/dist/component/contentTypeMutations.js +768 -0
  136. package/dist/component/contentTypeMutations.js.map +1 -0
  137. package/dist/component/contentTypes.d.ts +538 -0
  138. package/dist/component/contentTypes.d.ts.map +1 -0
  139. package/dist/component/contentTypes.js +304 -0
  140. package/dist/component/contentTypes.js.map +1 -0
  141. package/dist/component/convex.config.d.ts +42 -0
  142. package/dist/component/convex.config.d.ts.map +1 -0
  143. package/dist/component/convex.config.js +43 -0
  144. package/dist/component/convex.config.js.map +1 -0
  145. package/dist/component/documentTypes.d.ts +186 -0
  146. package/dist/component/documentTypes.d.ts.map +1 -0
  147. package/dist/component/documentTypes.js +23 -0
  148. package/dist/component/documentTypes.js.map +1 -0
  149. package/dist/component/eventEmitter.d.ts +281 -0
  150. package/dist/component/eventEmitter.d.ts.map +1 -0
  151. package/dist/component/eventEmitter.js +300 -0
  152. package/dist/component/eventEmitter.js.map +1 -0
  153. package/dist/component/exportImport.d.ts +1120 -0
  154. package/dist/component/exportImport.d.ts.map +1 -0
  155. package/dist/component/exportImport.js +931 -0
  156. package/dist/component/exportImport.js.map +1 -0
  157. package/dist/component/index.d.ts +28 -0
  158. package/dist/component/index.d.ts.map +1 -0
  159. package/dist/component/index.js +142 -0
  160. package/dist/component/index.js.map +1 -0
  161. package/dist/component/lib/deepReferenceResolver.d.ts +252 -0
  162. package/dist/component/lib/deepReferenceResolver.d.ts.map +1 -0
  163. package/dist/component/lib/deepReferenceResolver.js +601 -0
  164. package/dist/component/lib/deepReferenceResolver.js.map +1 -0
  165. package/dist/component/lib/errors.d.ts +306 -0
  166. package/dist/component/lib/errors.d.ts.map +1 -0
  167. package/dist/component/lib/errors.js +407 -0
  168. package/dist/component/lib/errors.js.map +1 -0
  169. package/dist/component/lib/index.d.ts +10 -0
  170. package/dist/component/lib/index.d.ts.map +1 -0
  171. package/dist/component/lib/index.js +33 -0
  172. package/dist/component/lib/index.js.map +1 -0
  173. package/dist/component/lib/mediaReferenceResolver.d.ts +217 -0
  174. package/dist/component/lib/mediaReferenceResolver.d.ts.map +1 -0
  175. package/dist/component/lib/mediaReferenceResolver.js +326 -0
  176. package/dist/component/lib/mediaReferenceResolver.js.map +1 -0
  177. package/dist/component/lib/metadataExtractor.d.ts +245 -0
  178. package/dist/component/lib/metadataExtractor.d.ts.map +1 -0
  179. package/dist/component/lib/metadataExtractor.js +548 -0
  180. package/dist/component/lib/metadataExtractor.js.map +1 -0
  181. package/dist/component/lib/mutationAuth.d.ts +95 -0
  182. package/dist/component/lib/mutationAuth.d.ts.map +1 -0
  183. package/dist/component/lib/mutationAuth.js +146 -0
  184. package/dist/component/lib/mutationAuth.js.map +1 -0
  185. package/dist/component/lib/queries.d.ts +17 -0
  186. package/dist/component/lib/queries.d.ts.map +1 -0
  187. package/dist/component/lib/queries.js +49 -0
  188. package/dist/component/lib/queries.js.map +1 -0
  189. package/dist/component/lib/ragContentChunker.d.ts +423 -0
  190. package/dist/component/lib/ragContentChunker.d.ts.map +1 -0
  191. package/dist/component/lib/ragContentChunker.js +897 -0
  192. package/dist/component/lib/ragContentChunker.js.map +1 -0
  193. package/dist/component/lib/referenceResolver.d.ts +175 -0
  194. package/dist/component/lib/referenceResolver.d.ts.map +1 -0
  195. package/dist/component/lib/referenceResolver.js +293 -0
  196. package/dist/component/lib/referenceResolver.js.map +1 -0
  197. package/dist/component/lib/slugGenerator.d.ts +71 -0
  198. package/dist/component/lib/slugGenerator.d.ts.map +1 -0
  199. package/dist/component/lib/slugGenerator.js +207 -0
  200. package/dist/component/lib/slugGenerator.js.map +1 -0
  201. package/dist/component/lib/slugUniqueness.d.ts +131 -0
  202. package/dist/component/lib/slugUniqueness.d.ts.map +1 -0
  203. package/dist/component/lib/slugUniqueness.js +229 -0
  204. package/dist/component/lib/slugUniqueness.js.map +1 -0
  205. package/dist/component/lib/softDelete.d.ts +18 -0
  206. package/dist/component/lib/softDelete.d.ts.map +1 -0
  207. package/dist/component/lib/softDelete.js +29 -0
  208. package/dist/component/lib/softDelete.js.map +1 -0
  209. package/dist/component/localeFallbackChain.d.ts +410 -0
  210. package/dist/component/localeFallbackChain.d.ts.map +1 -0
  211. package/dist/component/localeFallbackChain.js +467 -0
  212. package/dist/component/localeFallbackChain.js.map +1 -0
  213. package/dist/component/localeFields.d.ts +508 -0
  214. package/dist/component/localeFields.d.ts.map +1 -0
  215. package/dist/component/localeFields.js +592 -0
  216. package/dist/component/localeFields.js.map +1 -0
  217. package/dist/component/mediaAssetMutations.d.ts +235 -0
  218. package/dist/component/mediaAssetMutations.d.ts.map +1 -0
  219. package/dist/component/mediaAssetMutations.js +558 -0
  220. package/dist/component/mediaAssetMutations.js.map +1 -0
  221. package/dist/component/mediaAssets.d.ts +168 -0
  222. package/dist/component/mediaAssets.d.ts.map +1 -0
  223. package/dist/component/mediaAssets.js +618 -0
  224. package/dist/component/mediaAssets.js.map +1 -0
  225. package/dist/component/mediaFolderMutations.d.ts +642 -0
  226. package/dist/component/mediaFolderMutations.d.ts.map +1 -0
  227. package/dist/component/mediaFolderMutations.js +849 -0
  228. package/dist/component/mediaFolderMutations.js.map +1 -0
  229. package/dist/component/mediaUploadMutations.d.ts +136 -0
  230. package/dist/component/mediaUploadMutations.d.ts.map +1 -0
  231. package/dist/component/mediaUploadMutations.js +205 -0
  232. package/dist/component/mediaUploadMutations.js.map +1 -0
  233. package/dist/component/mediaVariantMutations.d.ts +468 -0
  234. package/dist/component/mediaVariantMutations.d.ts.map +1 -0
  235. package/dist/component/mediaVariantMutations.js +737 -0
  236. package/dist/component/mediaVariantMutations.js.map +1 -0
  237. package/dist/component/mediaVariants.d.ts +525 -0
  238. package/dist/component/mediaVariants.d.ts.map +1 -0
  239. package/dist/component/mediaVariants.js +661 -0
  240. package/dist/component/mediaVariants.js.map +1 -0
  241. package/dist/component/ragContentIndexer.d.ts +595 -0
  242. package/dist/component/ragContentIndexer.d.ts.map +1 -0
  243. package/dist/component/ragContentIndexer.js +794 -0
  244. package/dist/component/ragContentIndexer.js.map +1 -0
  245. package/dist/component/rateLimitHooks.d.ts +266 -0
  246. package/dist/component/rateLimitHooks.d.ts.map +1 -0
  247. package/dist/component/rateLimitHooks.js +412 -0
  248. package/dist/component/rateLimitHooks.js.map +1 -0
  249. package/dist/component/roles.d.ts +649 -0
  250. package/dist/component/roles.d.ts.map +1 -0
  251. package/dist/component/roles.js +884 -0
  252. package/dist/component/roles.js.map +1 -0
  253. package/dist/component/scheduledPublish.d.ts +182 -0
  254. package/dist/component/scheduledPublish.d.ts.map +1 -0
  255. package/dist/component/scheduledPublish.js +304 -0
  256. package/dist/component/scheduledPublish.js.map +1 -0
  257. package/dist/component/schema.d.ts +4114 -0
  258. package/dist/component/schema.d.ts.map +1 -0
  259. package/dist/component/schema.js +469 -0
  260. package/dist/component/schema.js.map +1 -0
  261. package/dist/component/taxonomies.d.ts +476 -0
  262. package/dist/component/taxonomies.d.ts.map +1 -0
  263. package/dist/component/taxonomies.js +785 -0
  264. package/dist/component/taxonomies.js.map +1 -0
  265. package/dist/component/taxonomyMutations.d.ts +206 -0
  266. package/dist/component/taxonomyMutations.d.ts.map +1 -0
  267. package/dist/component/taxonomyMutations.js +1001 -0
  268. package/dist/component/taxonomyMutations.js.map +1 -0
  269. package/dist/component/trash.d.ts +265 -0
  270. package/dist/component/trash.d.ts.map +1 -0
  271. package/dist/component/trash.js +621 -0
  272. package/dist/component/trash.js.map +1 -0
  273. package/dist/component/types.d.ts +4 -0
  274. package/dist/component/types.d.ts.map +1 -0
  275. package/dist/component/types.js +2 -0
  276. package/dist/component/types.js.map +1 -0
  277. package/dist/component/userContext.d.ts +508 -0
  278. package/dist/component/userContext.d.ts.map +1 -0
  279. package/dist/component/userContext.js +615 -0
  280. package/dist/component/userContext.js.map +1 -0
  281. package/dist/component/validation.d.ts +387 -0
  282. package/dist/component/validation.d.ts.map +1 -0
  283. package/dist/component/validation.js +1052 -0
  284. package/dist/component/validation.js.map +1 -0
  285. package/dist/component/validators.d.ts +4645 -0
  286. package/dist/component/validators.d.ts.map +1 -0
  287. package/dist/component/validators.js +641 -0
  288. package/dist/component/validators.js.map +1 -0
  289. package/dist/component/versionMutations.d.ts +216 -0
  290. package/dist/component/versionMutations.d.ts.map +1 -0
  291. package/dist/component/versionMutations.js +321 -0
  292. package/dist/component/versionMutations.js.map +1 -0
  293. package/dist/component/webhookTrigger.d.ts +770 -0
  294. package/dist/component/webhookTrigger.d.ts.map +1 -0
  295. package/dist/component/webhookTrigger.js +1413 -0
  296. package/dist/component/webhookTrigger.js.map +1 -0
  297. package/dist/react/index.d.ts +316 -0
  298. package/dist/react/index.d.ts.map +1 -0
  299. package/dist/react/index.js +558 -0
  300. package/dist/react/index.js.map +1 -0
  301. package/dist/test.d.ts +2230 -0
  302. package/dist/test.d.ts.map +1 -0
  303. package/dist/test.js +1107 -0
  304. package/dist/test.js.map +1 -0
  305. package/package.json +95 -0
  306. package/src/cli/commands/admin.ts +104 -0
  307. package/src/cli/index.ts +21 -0
  308. package/src/cli/utils/detectConvexUrl.ts +54 -0
  309. package/src/cli/utils/openBrowser.ts +16 -0
  310. package/src/client/admin-config.ts +138 -0
  311. package/src/client/adminApi.ts +942 -0
  312. package/src/client/agentTools.ts +1311 -0
  313. package/src/client/argTypes.ts +316 -0
  314. package/src/client/field-types.ts +187 -0
  315. package/src/client/index.ts +1301 -0
  316. package/src/client/queryBuilder.ts +1100 -0
  317. package/src/client/schema/codegen.ts +500 -0
  318. package/src/client/schema/defineContentType.ts +501 -0
  319. package/src/client/schema/index.ts +169 -0
  320. package/src/client/schema/schemaDrift.ts +574 -0
  321. package/src/client/schema/typedClient.ts +688 -0
  322. package/src/client/schema/types.ts +666 -0
  323. package/src/client/types.ts +723 -0
  324. package/src/client/workflows.ts +141 -0
  325. package/src/client/wrapper.ts +4304 -0
  326. package/src/component/_generated/api.ts +140 -0
  327. package/src/component/_generated/component.ts +5029 -0
  328. package/src/component/_generated/dataModel.ts +60 -0
  329. package/src/component/_generated/server.ts +156 -0
  330. package/src/component/authorization.ts +647 -0
  331. package/src/component/authorizationHooks.ts +668 -0
  332. package/src/component/bulkOperations.ts +687 -0
  333. package/src/component/contentEntries.ts +1976 -0
  334. package/src/component/contentEntryMutations.ts +1223 -0
  335. package/src/component/contentEntryValidation.ts +707 -0
  336. package/src/component/contentLock.ts +550 -0
  337. package/src/component/contentTypeMigration.ts +1064 -0
  338. package/src/component/contentTypeMutations.ts +969 -0
  339. package/src/component/contentTypes.ts +346 -0
  340. package/src/component/convex.config.ts +44 -0
  341. package/src/component/documentTypes.ts +240 -0
  342. package/src/component/eventEmitter.ts +485 -0
  343. package/src/component/exportImport.ts +1169 -0
  344. package/src/component/index.ts +491 -0
  345. package/src/component/lib/deepReferenceResolver.ts +999 -0
  346. package/src/component/lib/errors.ts +816 -0
  347. package/src/component/lib/index.ts +145 -0
  348. package/src/component/lib/mediaReferenceResolver.ts +495 -0
  349. package/src/component/lib/metadataExtractor.ts +792 -0
  350. package/src/component/lib/mutationAuth.ts +199 -0
  351. package/src/component/lib/queries.ts +79 -0
  352. package/src/component/lib/ragContentChunker.ts +1371 -0
  353. package/src/component/lib/referenceResolver.ts +430 -0
  354. package/src/component/lib/slugGenerator.ts +262 -0
  355. package/src/component/lib/slugUniqueness.ts +333 -0
  356. package/src/component/lib/softDelete.ts +44 -0
  357. package/src/component/localeFallbackChain.ts +673 -0
  358. package/src/component/localeFields.ts +896 -0
  359. package/src/component/mediaAssetMutations.ts +725 -0
  360. package/src/component/mediaAssets.ts +932 -0
  361. package/src/component/mediaFolderMutations.ts +1046 -0
  362. package/src/component/mediaUploadMutations.ts +224 -0
  363. package/src/component/mediaVariantMutations.ts +900 -0
  364. package/src/component/mediaVariants.ts +793 -0
  365. package/src/component/ragContentIndexer.ts +1067 -0
  366. package/src/component/rateLimitHooks.ts +572 -0
  367. package/src/component/roles.ts +1360 -0
  368. package/src/component/scheduledPublish.ts +358 -0
  369. package/src/component/schema.ts +617 -0
  370. package/src/component/taxonomies.ts +949 -0
  371. package/src/component/taxonomyMutations.ts +1210 -0
  372. package/src/component/trash.ts +724 -0
  373. package/src/component/userContext.ts +898 -0
  374. package/src/component/validation.ts +1388 -0
  375. package/src/component/validators.ts +949 -0
  376. package/src/component/versionMutations.ts +392 -0
  377. package/src/component/webhookTrigger.ts +1922 -0
  378. package/src/react/index.ts +898 -0
  379. 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
+ }