@contember/bindx-uploader 0.1.0

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 (127) hide show
  1. package/dist/UploaderError.d.ts +6 -0
  2. package/dist/UploaderError.d.ts.map +1 -0
  3. package/dist/components/MultiUploader.d.ts +43 -0
  4. package/dist/components/MultiUploader.d.ts.map +1 -0
  5. package/dist/components/Uploader.d.ts +58 -0
  6. package/dist/components/Uploader.d.ts.map +1 -0
  7. package/dist/components/UploaderDropzoneArea.d.ts +6 -0
  8. package/dist/components/UploaderDropzoneArea.d.ts.map +1 -0
  9. package/dist/components/UploaderDropzoneRoot.d.ts +8 -0
  10. package/dist/components/UploaderDropzoneRoot.d.ts.map +1 -0
  11. package/dist/components/UploaderEachFile.d.ts +14 -0
  12. package/dist/components/UploaderEachFile.d.ts.map +1 -0
  13. package/dist/components/UploaderFileStateSwitch.d.ts +24 -0
  14. package/dist/components/UploaderFileStateSwitch.d.ts.map +1 -0
  15. package/dist/components/UploaderHasFile.d.ts +14 -0
  16. package/dist/components/UploaderHasFile.d.ts.map +1 -0
  17. package/dist/components/index.d.ts +8 -0
  18. package/dist/components/index.d.ts.map +1 -0
  19. package/dist/contexts.d.ts +32 -0
  20. package/dist/contexts.d.ts.map +1 -0
  21. package/dist/extractors/getAudioFileDataExtractor.d.ts +9 -0
  22. package/dist/extractors/getAudioFileDataExtractor.d.ts.map +1 -0
  23. package/dist/extractors/getFileUrlDataExtractor.d.ts +9 -0
  24. package/dist/extractors/getFileUrlDataExtractor.d.ts.map +1 -0
  25. package/dist/extractors/getGenericFileMetadataExtractor.d.ts +12 -0
  26. package/dist/extractors/getGenericFileMetadataExtractor.d.ts.map +1 -0
  27. package/dist/extractors/getImageFileDataExtractor.d.ts +10 -0
  28. package/dist/extractors/getImageFileDataExtractor.d.ts.map +1 -0
  29. package/dist/extractors/getVideoFileDataExtractor.d.ts +11 -0
  30. package/dist/extractors/getVideoFileDataExtractor.d.ts.map +1 -0
  31. package/dist/extractors/index.d.ts +6 -0
  32. package/dist/extractors/index.d.ts.map +1 -0
  33. package/dist/extractors/types.d.ts +13 -0
  34. package/dist/extractors/types.d.ts.map +1 -0
  35. package/dist/fileTypes/createAnyFileType.d.ts +27 -0
  36. package/dist/fileTypes/createAnyFileType.d.ts.map +1 -0
  37. package/dist/fileTypes/createAudioFileType.d.ts +29 -0
  38. package/dist/fileTypes/createAudioFileType.d.ts.map +1 -0
  39. package/dist/fileTypes/createImageFileType.d.ts +47 -0
  40. package/dist/fileTypes/createImageFileType.d.ts.map +1 -0
  41. package/dist/fileTypes/createVideoFileType.d.ts +33 -0
  42. package/dist/fileTypes/createVideoFileType.d.ts.map +1 -0
  43. package/dist/fileTypes/index.d.ts +5 -0
  44. package/dist/fileTypes/index.d.ts.map +1 -0
  45. package/dist/hooks/index.d.ts +3 -0
  46. package/dist/hooks/index.d.ts.map +1 -0
  47. package/dist/hooks/useS3Client.d.ts +7 -0
  48. package/dist/hooks/useS3Client.d.ts.map +1 -0
  49. package/dist/hooks/useUploaderStateFiles.d.ts +8 -0
  50. package/dist/hooks/useUploaderStateFiles.d.ts.map +1 -0
  51. package/dist/index.d.ts +10 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/internal/hooks/index.d.ts +5 -0
  54. package/dist/internal/hooks/index.d.ts.map +1 -0
  55. package/dist/internal/hooks/useFillEntity.d.ts +17 -0
  56. package/dist/internal/hooks/useFillEntity.d.ts.map +1 -0
  57. package/dist/internal/hooks/useGetPreviewUrls.d.ts +6 -0
  58. package/dist/internal/hooks/useGetPreviewUrls.d.ts.map +1 -0
  59. package/dist/internal/hooks/useUploadState.d.ts +12 -0
  60. package/dist/internal/hooks/useUploadState.d.ts.map +1 -0
  61. package/dist/internal/hooks/useUploaderDoUpload.d.ts +7 -0
  62. package/dist/internal/hooks/useUploaderDoUpload.d.ts.map +1 -0
  63. package/dist/internal/utils/executeExtractors.d.ts +16 -0
  64. package/dist/internal/utils/executeExtractors.d.ts.map +1 -0
  65. package/dist/internal/utils/index.d.ts +4 -0
  66. package/dist/internal/utils/index.d.ts.map +1 -0
  67. package/dist/internal/utils/resolveAccept.d.ts +7 -0
  68. package/dist/internal/utils/resolveAccept.d.ts.map +1 -0
  69. package/dist/internal/utils/uploaderErrorHandler.d.ts +7 -0
  70. package/dist/internal/utils/uploaderErrorHandler.d.ts.map +1 -0
  71. package/dist/types.d.ts +168 -0
  72. package/dist/types.d.ts.map +1 -0
  73. package/dist/uploadClient/S3UploadClient.d.ts +19 -0
  74. package/dist/uploadClient/S3UploadClient.d.ts.map +1 -0
  75. package/dist/uploadClient/index.d.ts +3 -0
  76. package/dist/uploadClient/index.d.ts.map +1 -0
  77. package/dist/uploadClient/types.d.ts +29 -0
  78. package/dist/uploadClient/types.d.ts.map +1 -0
  79. package/dist/utils/attrAccept.d.ts +16 -0
  80. package/dist/utils/attrAccept.d.ts.map +1 -0
  81. package/dist/utils/index.d.ts +3 -0
  82. package/dist/utils/index.d.ts.map +1 -0
  83. package/dist/utils/urlSigner.d.ts +8 -0
  84. package/dist/utils/urlSigner.d.ts.map +1 -0
  85. package/package.json +35 -0
  86. package/src/UploaderError.ts +7 -0
  87. package/src/components/MultiUploader.tsx +210 -0
  88. package/src/components/Uploader.tsx +174 -0
  89. package/src/components/UploaderDropzoneArea.tsx +29 -0
  90. package/src/components/UploaderDropzoneRoot.tsx +32 -0
  91. package/src/components/UploaderEachFile.tsx +46 -0
  92. package/src/components/UploaderFileStateSwitch.tsx +66 -0
  93. package/src/components/UploaderHasFile.tsx +35 -0
  94. package/src/components/index.ts +7 -0
  95. package/src/contexts.ts +76 -0
  96. package/src/extractors/getAudioFileDataExtractor.ts +44 -0
  97. package/src/extractors/getFileUrlDataExtractor.ts +22 -0
  98. package/src/extractors/getGenericFileMetadataExtractor.ts +40 -0
  99. package/src/extractors/getImageFileDataExtractor.ts +50 -0
  100. package/src/extractors/getVideoFileDataExtractor.ts +60 -0
  101. package/src/extractors/index.ts +5 -0
  102. package/src/extractors/types.ts +14 -0
  103. package/src/fileTypes/createAnyFileType.ts +56 -0
  104. package/src/fileTypes/createAudioFileType.ts +67 -0
  105. package/src/fileTypes/createImageFileType.ts +87 -0
  106. package/src/fileTypes/createVideoFileType.ts +75 -0
  107. package/src/fileTypes/index.ts +4 -0
  108. package/src/hooks/index.ts +2 -0
  109. package/src/hooks/useS3Client.ts +26 -0
  110. package/src/hooks/useUploaderStateFiles.ts +22 -0
  111. package/src/index.ts +126 -0
  112. package/src/internal/hooks/index.ts +4 -0
  113. package/src/internal/hooks/useFillEntity.ts +89 -0
  114. package/src/internal/hooks/useGetPreviewUrls.ts +26 -0
  115. package/src/internal/hooks/useUploadState.ts +159 -0
  116. package/src/internal/hooks/useUploaderDoUpload.ts +129 -0
  117. package/src/internal/utils/executeExtractors.ts +46 -0
  118. package/src/internal/utils/index.ts +3 -0
  119. package/src/internal/utils/resolveAccept.ts +26 -0
  120. package/src/internal/utils/uploaderErrorHandler.ts +15 -0
  121. package/src/types.ts +242 -0
  122. package/src/uploadClient/S3UploadClient.ts +119 -0
  123. package/src/uploadClient/index.ts +2 -0
  124. package/src/uploadClient/types.ts +30 -0
  125. package/src/utils/attrAccept.ts +87 -0
  126. package/src/utils/index.ts +2 -0
  127. package/src/utils/urlSigner.ts +117 -0
@@ -0,0 +1,44 @@
1
+ import type { FieldRef } from '@contember/bindx'
2
+ import type { FieldName, FileDataExtractor } from '../types.js'
3
+
4
+ export interface AudioFileDataExtractorProps<TEntity> {
5
+ durationField?: FieldName<TEntity>
6
+ }
7
+
8
+ /**
9
+ * Creates an extractor that extracts audio duration.
10
+ */
11
+ export const getAudioFileDataExtractor = <TEntity extends Record<string, unknown>>({
12
+ durationField,
13
+ }: AudioFileDataExtractorProps<TEntity>): FileDataExtractor<TEntity> => ({
14
+ getFieldNames: () => durationField ? [durationField] : [],
15
+ extractFileData: async ({ previewUrl }) => {
16
+ if (!durationField) {
17
+ return undefined
18
+ }
19
+
20
+ const result = await new Promise<{ duration: number }>((resolve, reject) => {
21
+ const audio = document.createElement('audio')
22
+ audio.preload = 'metadata'
23
+
24
+ audio.addEventListener('canplay', () => {
25
+ resolve({
26
+ duration: audio.duration,
27
+ })
28
+ })
29
+ audio.addEventListener('error', e => {
30
+ reject(e)
31
+ })
32
+ audio.src = previewUrl
33
+ })
34
+
35
+ return ({ entity }) => {
36
+ const fields = (entity as { $fields: Record<string, FieldRef<unknown>> }).$fields
37
+
38
+ if (durationField) {
39
+ // Round duration for integer fields
40
+ fields[durationField]?.setValue(Math.round(result.duration))
41
+ }
42
+ }
43
+ },
44
+ })
@@ -0,0 +1,22 @@
1
+ import type { FieldRef } from '@contember/bindx'
2
+ import type { FieldName, FileDataExtractor } from '../types.js'
3
+
4
+ export interface FileUrlDataExtractorProps<TEntity> {
5
+ urlField: FieldName<TEntity>
6
+ }
7
+
8
+ /**
9
+ * Creates an extractor that populates the URL field with the upload result's public URL.
10
+ */
11
+ export const getFileUrlDataExtractor = <TEntity extends Record<string, unknown>>({
12
+ urlField,
13
+ }: FileUrlDataExtractorProps<TEntity>): FileDataExtractor<TEntity> => ({
14
+ getFieldNames: () => [urlField],
15
+ populateFields: ({ entity, result }) => {
16
+ const fields = (entity as { $fields: Record<string, FieldRef<unknown>> }).$fields
17
+ const field = fields[urlField]
18
+ if (field) {
19
+ field.setValue(result.publicUrl ?? null)
20
+ }
21
+ },
22
+ })
@@ -0,0 +1,40 @@
1
+ import type { FieldRef } from '@contember/bindx'
2
+ import type { FieldName, FileDataExtractor } from '../types.js'
3
+
4
+ export interface GenericFileMetadataExtractorProps<TEntity> {
5
+ fileNameField?: FieldName<TEntity>
6
+ lastModifiedField?: FieldName<TEntity>
7
+ fileSizeField?: FieldName<TEntity>
8
+ fileTypeField?: FieldName<TEntity>
9
+ }
10
+
11
+ /**
12
+ * Creates an extractor that populates generic file metadata fields.
13
+ */
14
+ export const getGenericFileMetadataExtractor = <TEntity extends Record<string, unknown>>({
15
+ fileNameField,
16
+ fileSizeField,
17
+ fileTypeField,
18
+ lastModifiedField,
19
+ }: GenericFileMetadataExtractorProps<TEntity>): FileDataExtractor<TEntity> => ({
20
+ getFieldNames: () => [fileNameField, fileSizeField, fileTypeField, lastModifiedField].filter((f): f is FieldName<TEntity> => !!f),
21
+ extractFileData: ({ file }) => {
22
+ return ({ entity }) => {
23
+ const fields = (entity as { $fields: Record<string, FieldRef<unknown>> }).$fields
24
+
25
+ if (fileNameField) {
26
+ fields[fileNameField]?.setValue(file.name)
27
+ }
28
+ if (fileSizeField) {
29
+ fields[fileSizeField]?.setValue(file.size)
30
+ }
31
+ if (fileTypeField) {
32
+ fields[fileTypeField]?.setValue(file.type)
33
+ }
34
+ if (lastModifiedField) {
35
+ // Convert to ISO string for Date/DateTime fields
36
+ fields[lastModifiedField]?.setValue(new Date(file.lastModified).toISOString())
37
+ }
38
+ }
39
+ },
40
+ })
@@ -0,0 +1,50 @@
1
+ import type { FieldRef } from '@contember/bindx'
2
+ import type { FieldName, FileDataExtractor } from '../types.js'
3
+
4
+ export interface ImageFileDataExtractorProps<TEntity> {
5
+ widthField?: FieldName<TEntity>
6
+ heightField?: FieldName<TEntity>
7
+ }
8
+
9
+ /**
10
+ * Creates an extractor that extracts image dimensions (width, height).
11
+ */
12
+ export const getImageFileDataExtractor = <TEntity extends Record<string, unknown>>({
13
+ widthField,
14
+ heightField,
15
+ }: ImageFileDataExtractorProps<TEntity>): FileDataExtractor<TEntity> => ({
16
+ getFieldNames: () => [widthField, heightField].filter((f): f is FieldName<TEntity> => !!f),
17
+ extractFileData: async ({ previewUrl }) => {
18
+ if (!heightField && !widthField) {
19
+ return undefined
20
+ }
21
+
22
+ const result = await new Promise<{
23
+ width: number
24
+ height: number
25
+ }>((resolve, reject) => {
26
+ const image = new Image()
27
+ image.addEventListener('load', () => {
28
+ resolve({
29
+ width: image.naturalWidth,
30
+ height: image.naturalHeight,
31
+ })
32
+ })
33
+ image.addEventListener('error', e => {
34
+ reject(e)
35
+ })
36
+ image.src = previewUrl
37
+ })
38
+
39
+ return ({ entity }) => {
40
+ const fields = (entity as { $fields: Record<string, FieldRef<unknown>> }).$fields
41
+
42
+ if (widthField) {
43
+ fields[widthField]?.setValue(result.width ?? null)
44
+ }
45
+ if (heightField) {
46
+ fields[heightField]?.setValue(result.height ?? null)
47
+ }
48
+ }
49
+ },
50
+ })
@@ -0,0 +1,60 @@
1
+ import type { FieldRef } from '@contember/bindx'
2
+ import type { FieldName, FileDataExtractor } from '../types.js'
3
+
4
+ export interface VideoFileDataExtractorProps<TEntity> {
5
+ widthField?: FieldName<TEntity>
6
+ heightField?: FieldName<TEntity>
7
+ durationField?: FieldName<TEntity>
8
+ }
9
+
10
+ /**
11
+ * Creates an extractor that extracts video metadata (dimensions, duration).
12
+ */
13
+ export const getVideoFileDataExtractor = <TEntity extends Record<string, unknown>>({
14
+ widthField,
15
+ heightField,
16
+ durationField,
17
+ }: VideoFileDataExtractorProps<TEntity>): FileDataExtractor<TEntity> => ({
18
+ getFieldNames: () => [widthField, heightField, durationField].filter((f): f is FieldName<TEntity> => !!f),
19
+ extractFileData: async ({ previewUrl }) => {
20
+ if (!heightField && !widthField && !durationField) {
21
+ return undefined
22
+ }
23
+
24
+ const result = await new Promise<{
25
+ width: number
26
+ height: number
27
+ duration: number
28
+ }>((resolve, reject) => {
29
+ const video = document.createElement('video')
30
+ video.preload = 'metadata'
31
+
32
+ video.addEventListener('loadedmetadata', () => {
33
+ resolve({
34
+ width: video.videoWidth,
35
+ height: video.videoHeight,
36
+ duration: video.duration,
37
+ })
38
+ })
39
+ video.addEventListener('error', e => {
40
+ reject(e)
41
+ })
42
+ video.src = previewUrl
43
+ })
44
+
45
+ return ({ entity }) => {
46
+ const fields = (entity as { $fields: Record<string, FieldRef<unknown>> }).$fields
47
+
48
+ if (widthField) {
49
+ fields[widthField]?.setValue(result.width ?? null)
50
+ }
51
+ if (heightField) {
52
+ fields[heightField]?.setValue(result.height ?? null)
53
+ }
54
+ if (durationField) {
55
+ // Round duration for integer fields
56
+ fields[durationField]?.setValue(Math.round(result.duration))
57
+ }
58
+ }
59
+ },
60
+ })
@@ -0,0 +1,5 @@
1
+ export { getFileUrlDataExtractor, type FileUrlDataExtractorProps } from './getFileUrlDataExtractor.js'
2
+ export { getGenericFileMetadataExtractor, type GenericFileMetadataExtractorProps } from './getGenericFileMetadataExtractor.js'
3
+ export { getImageFileDataExtractor, type ImageFileDataExtractorProps } from './getImageFileDataExtractor.js'
4
+ export { getVideoFileDataExtractor, type VideoFileDataExtractorProps } from './getVideoFileDataExtractor.js'
5
+ export { getAudioFileDataExtractor, type AudioFileDataExtractorProps } from './getAudioFileDataExtractor.js'
@@ -0,0 +1,14 @@
1
+ import type { FieldRef } from '@contember/bindx'
2
+ import type { FileWithMeta, FileUploadResult, FileDataExtractor, FileDataExtractorPopulator } from '../types.js'
3
+
4
+ /**
5
+ * Generic entity type for extractors - allows any string field access
6
+ */
7
+ export type ExtractorEntity = {
8
+ $fields: Record<string, FieldRef<unknown>>
9
+ }
10
+
11
+ /**
12
+ * Helper type for extractor factory function
13
+ */
14
+ export type ExtractorFactory<TProps, TEntity = ExtractorEntity> = (props: TProps) => FileDataExtractor<TEntity>
@@ -0,0 +1,56 @@
1
+ import type { FieldName, FileType, UploadClient } from '../types.js'
2
+ import { getFileUrlDataExtractor } from '../extractors/getFileUrlDataExtractor.js'
3
+ import { getGenericFileMetadataExtractor } from '../extractors/getGenericFileMetadataExtractor.js'
4
+
5
+ export interface CreateAnyFileTypeProps<TEntity> {
6
+ /** Upload client to use */
7
+ uploader?: UploadClient<unknown>
8
+ /** Field name for the URL */
9
+ urlField: FieldName<TEntity>
10
+ /** Field name for file name (optional) */
11
+ fileNameField?: FieldName<TEntity>
12
+ /** Field name for file size (optional) */
13
+ fileSizeField?: FieldName<TEntity>
14
+ /** Field name for file type/MIME (optional) */
15
+ fileTypeField?: FieldName<TEntity>
16
+ /** Field name for last modified (optional) */
17
+ lastModifiedField?: FieldName<TEntity>
18
+ /** Additional extractors */
19
+ extractors?: FileType<TEntity>['extractors']
20
+ /** Custom file validator */
21
+ acceptFile?: FileType<TEntity>['acceptFile']
22
+ /** Accept MIME types (optional - defaults to any) */
23
+ accept?: FileType<TEntity>['accept']
24
+ }
25
+
26
+ /**
27
+ * Creates a file type configuration for any file upload.
28
+ * Includes extractors for URL and generic metadata only.
29
+ */
30
+ export const createAnyFileType = <TEntity extends Record<string, unknown>>({
31
+ uploader,
32
+ urlField,
33
+ fileNameField,
34
+ fileSizeField,
35
+ fileTypeField,
36
+ lastModifiedField,
37
+ extractors = [],
38
+ acceptFile,
39
+ accept,
40
+ }: CreateAnyFileTypeProps<TEntity>): FileType<TEntity> => ({
41
+ accept,
42
+ acceptFile,
43
+ uploader,
44
+ extractors: [
45
+ getGenericFileMetadataExtractor<TEntity>({
46
+ fileNameField,
47
+ fileSizeField,
48
+ fileTypeField,
49
+ lastModifiedField,
50
+ }),
51
+ getFileUrlDataExtractor<TEntity>({
52
+ urlField,
53
+ }),
54
+ ...extractors,
55
+ ],
56
+ })
@@ -0,0 +1,67 @@
1
+ import type { FieldName, FileType, UploadClient } from '../types.js'
2
+ import { getFileUrlDataExtractor } from '../extractors/getFileUrlDataExtractor.js'
3
+ import { getGenericFileMetadataExtractor } from '../extractors/getGenericFileMetadataExtractor.js'
4
+ import { getAudioFileDataExtractor } from '../extractors/getAudioFileDataExtractor.js'
5
+
6
+ export interface CreateAudioFileTypeProps<TEntity> {
7
+ /** Upload client to use */
8
+ uploader?: UploadClient<unknown>
9
+ /** Field name for the URL */
10
+ urlField: FieldName<TEntity>
11
+ /** Field name for duration (optional) */
12
+ durationField?: FieldName<TEntity>
13
+ /** Field name for file name (optional) */
14
+ fileNameField?: FieldName<TEntity>
15
+ /** Field name for file size (optional) */
16
+ fileSizeField?: FieldName<TEntity>
17
+ /** Field name for file type/MIME (optional) */
18
+ fileTypeField?: FieldName<TEntity>
19
+ /** Field name for last modified (optional) */
20
+ lastModifiedField?: FieldName<TEntity>
21
+ /** Additional extractors */
22
+ extractors?: FileType<TEntity>['extractors']
23
+ /** Custom file validator */
24
+ acceptFile?: FileType<TEntity>['acceptFile']
25
+ /** Override default accept MIME types */
26
+ accept?: FileType<TEntity>['accept']
27
+ }
28
+
29
+ const DEFAULT_AUDIO_ACCEPT = {
30
+ 'audio/*': ['.mp3', '.wav', '.ogg', '.flac', '.m4a', '.aac', '.wma', '.aiff'],
31
+ }
32
+
33
+ /**
34
+ * Creates a file type configuration for audio uploads.
35
+ * Includes extractors for URL, audio duration, and generic metadata.
36
+ */
37
+ export const createAudioFileType = <TEntity extends Record<string, unknown>>({
38
+ uploader,
39
+ urlField,
40
+ durationField,
41
+ fileNameField,
42
+ fileSizeField,
43
+ fileTypeField,
44
+ lastModifiedField,
45
+ extractors = [],
46
+ acceptFile,
47
+ accept = DEFAULT_AUDIO_ACCEPT,
48
+ }: CreateAudioFileTypeProps<TEntity>): FileType<TEntity> => ({
49
+ accept,
50
+ acceptFile,
51
+ uploader,
52
+ extractors: [
53
+ getGenericFileMetadataExtractor<TEntity>({
54
+ fileNameField,
55
+ fileSizeField,
56
+ fileTypeField,
57
+ lastModifiedField,
58
+ }),
59
+ getAudioFileDataExtractor<TEntity>({
60
+ durationField,
61
+ }),
62
+ getFileUrlDataExtractor<TEntity>({
63
+ urlField,
64
+ }),
65
+ ...extractors,
66
+ ],
67
+ })
@@ -0,0 +1,87 @@
1
+ import type { FieldName, FileType, UploadClient } from '../types.js'
2
+ import { getFileUrlDataExtractor } from '../extractors/getFileUrlDataExtractor.js'
3
+ import { getGenericFileMetadataExtractor } from '../extractors/getGenericFileMetadataExtractor.js'
4
+ import { getImageFileDataExtractor } from '../extractors/getImageFileDataExtractor.js'
5
+
6
+ export interface CreateImageFileTypeProps<TEntity> {
7
+ /** Upload client to use */
8
+ uploader?: UploadClient<unknown>
9
+ /** Field name for the URL */
10
+ urlField: FieldName<TEntity>
11
+ /** Field name for width (optional) */
12
+ widthField?: FieldName<TEntity>
13
+ /** Field name for height (optional) */
14
+ heightField?: FieldName<TEntity>
15
+ /** Field name for file name (optional) */
16
+ fileNameField?: FieldName<TEntity>
17
+ /** Field name for file size (optional) */
18
+ fileSizeField?: FieldName<TEntity>
19
+ /** Field name for file type/MIME (optional) */
20
+ fileTypeField?: FieldName<TEntity>
21
+ /** Field name for last modified (optional) */
22
+ lastModifiedField?: FieldName<TEntity>
23
+ /** Additional extractors */
24
+ extractors?: FileType<TEntity>['extractors']
25
+ /** Custom file validator */
26
+ acceptFile?: FileType<TEntity>['acceptFile']
27
+ /** Override default accept MIME types */
28
+ accept?: FileType<TEntity>['accept']
29
+ }
30
+
31
+ const DEFAULT_IMAGE_ACCEPT = {
32
+ 'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp'],
33
+ }
34
+
35
+ /**
36
+ * Creates a file type configuration for image uploads.
37
+ * Includes extractors for URL, image dimensions, and generic metadata.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * interface Image {
42
+ * id: string
43
+ * url: string
44
+ * width: number
45
+ * height: number
46
+ * }
47
+ *
48
+ * const imageFileType = createImageFileType<Image>({
49
+ * urlField: 'url', // Type-checked: must be keyof Image
50
+ * widthField: 'width', // Type-checked
51
+ * heightField: 'height',
52
+ * })
53
+ * ```
54
+ */
55
+ export const createImageFileType = <TEntity extends Record<string, unknown>>({
56
+ uploader,
57
+ urlField,
58
+ widthField,
59
+ heightField,
60
+ fileNameField,
61
+ fileSizeField,
62
+ fileTypeField,
63
+ lastModifiedField,
64
+ extractors = [],
65
+ acceptFile,
66
+ accept = DEFAULT_IMAGE_ACCEPT,
67
+ }: CreateImageFileTypeProps<TEntity>): FileType<TEntity> => ({
68
+ accept,
69
+ acceptFile,
70
+ uploader,
71
+ extractors: [
72
+ getGenericFileMetadataExtractor<TEntity>({
73
+ fileNameField,
74
+ fileSizeField,
75
+ fileTypeField,
76
+ lastModifiedField,
77
+ }),
78
+ getImageFileDataExtractor<TEntity>({
79
+ widthField,
80
+ heightField,
81
+ }),
82
+ getFileUrlDataExtractor<TEntity>({
83
+ urlField,
84
+ }),
85
+ ...extractors,
86
+ ],
87
+ })
@@ -0,0 +1,75 @@
1
+ import type { FieldName, FileType, UploadClient } from '../types.js'
2
+ import { getFileUrlDataExtractor } from '../extractors/getFileUrlDataExtractor.js'
3
+ import { getGenericFileMetadataExtractor } from '../extractors/getGenericFileMetadataExtractor.js'
4
+ import { getVideoFileDataExtractor } from '../extractors/getVideoFileDataExtractor.js'
5
+
6
+ export interface CreateVideoFileTypeProps<TEntity> {
7
+ /** Upload client to use */
8
+ uploader?: UploadClient<unknown>
9
+ /** Field name for the URL */
10
+ urlField: FieldName<TEntity>
11
+ /** Field name for width (optional) */
12
+ widthField?: FieldName<TEntity>
13
+ /** Field name for height (optional) */
14
+ heightField?: FieldName<TEntity>
15
+ /** Field name for duration (optional) */
16
+ durationField?: FieldName<TEntity>
17
+ /** Field name for file name (optional) */
18
+ fileNameField?: FieldName<TEntity>
19
+ /** Field name for file size (optional) */
20
+ fileSizeField?: FieldName<TEntity>
21
+ /** Field name for file type/MIME (optional) */
22
+ fileTypeField?: FieldName<TEntity>
23
+ /** Field name for last modified (optional) */
24
+ lastModifiedField?: FieldName<TEntity>
25
+ /** Additional extractors */
26
+ extractors?: FileType<TEntity>['extractors']
27
+ /** Custom file validator */
28
+ acceptFile?: FileType<TEntity>['acceptFile']
29
+ /** Override default accept MIME types */
30
+ accept?: FileType<TEntity>['accept']
31
+ }
32
+
33
+ const DEFAULT_VIDEO_ACCEPT = {
34
+ 'video/*': ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.wmv', '.mkv', '.3gp'],
35
+ }
36
+
37
+ /**
38
+ * Creates a file type configuration for video uploads.
39
+ * Includes extractors for URL, video dimensions, duration, and generic metadata.
40
+ */
41
+ export const createVideoFileType = <TEntity extends Record<string, unknown>>({
42
+ uploader,
43
+ urlField,
44
+ widthField,
45
+ heightField,
46
+ durationField,
47
+ fileNameField,
48
+ fileSizeField,
49
+ fileTypeField,
50
+ lastModifiedField,
51
+ extractors = [],
52
+ acceptFile,
53
+ accept = DEFAULT_VIDEO_ACCEPT,
54
+ }: CreateVideoFileTypeProps<TEntity>): FileType<TEntity> => ({
55
+ accept,
56
+ acceptFile,
57
+ uploader,
58
+ extractors: [
59
+ getGenericFileMetadataExtractor<TEntity>({
60
+ fileNameField,
61
+ fileSizeField,
62
+ fileTypeField,
63
+ lastModifiedField,
64
+ }),
65
+ getVideoFileDataExtractor<TEntity>({
66
+ widthField,
67
+ heightField,
68
+ durationField,
69
+ }),
70
+ getFileUrlDataExtractor<TEntity>({
71
+ urlField,
72
+ }),
73
+ ...extractors,
74
+ ],
75
+ })
@@ -0,0 +1,4 @@
1
+ export { createImageFileType, type CreateImageFileTypeProps } from './createImageFileType.js'
2
+ export { createVideoFileType, type CreateVideoFileTypeProps } from './createVideoFileType.js'
3
+ export { createAudioFileType, type CreateAudioFileTypeProps } from './createAudioFileType.js'
4
+ export { createAnyFileType, type CreateAnyFileTypeProps } from './createAnyFileType.js'
@@ -0,0 +1,2 @@
1
+ export { useUploaderStateFiles, type StateFilter } from './useUploaderStateFiles.js'
2
+ export { useS3Client } from './useS3Client.js'
@@ -0,0 +1,26 @@
1
+ import { useMemo } from 'react'
2
+ import { useBindxContext } from '@contember/bindx-react'
3
+ import { S3UploadClient, type S3UploadClientOptions } from '../uploadClient/S3UploadClient.js'
4
+ import { createContentApiS3Signer } from '../utils/urlSigner.js'
5
+
6
+ /**
7
+ * Creates an S3 upload client using the current Contember GraphQL client.
8
+ * Uses the bindx context to get the GraphQL client for URL signing.
9
+ */
10
+ export const useS3Client = (options: Partial<S3UploadClientOptions> = {}): S3UploadClient => {
11
+ const { adapter } = useBindxContext()
12
+
13
+ return useMemo(() => {
14
+ // Get the GraphQL client from the adapter
15
+ // ContemberAdapter has a graphQlClient property
16
+ const graphQlClient = (adapter as { graphQlClient?: { execute: (query: string, options?: unknown) => Promise<unknown> } }).graphQlClient
17
+ if (!graphQlClient) {
18
+ throw new Error('useS3Client requires a Contember adapter with GraphQL client')
19
+ }
20
+
21
+ return new S3UploadClient({
22
+ signUrl: createContentApiS3Signer(graphQlClient as Parameters<typeof createContentApiS3Signer>[0]),
23
+ ...options,
24
+ })
25
+ }, [adapter, options])
26
+ }
@@ -0,0 +1,22 @@
1
+ import { useMemo } from 'react'
2
+ import type { UploaderFileState } from '../types.js'
3
+ import { useUploaderState } from '../contexts.js'
4
+
5
+ export type StateFilter = UploaderFileState['state'] | UploaderFileState['state'][]
6
+
7
+ /**
8
+ * Filters uploader state by state type.
9
+ * Returns an array of matching file states.
10
+ */
11
+ export const useUploaderStateFiles = (stateFilter?: StateFilter): UploaderFileState[] => {
12
+ const files = useUploaderState()
13
+
14
+ return useMemo(() => {
15
+ if (!stateFilter) {
16
+ return files
17
+ }
18
+
19
+ const filterArray = Array.isArray(stateFilter) ? stateFilter : [stateFilter]
20
+ return files.filter(file => filterArray.includes(file.state))
21
+ }, [files, stateFilter])
22
+ }