@susonwaiba/react-media-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.
@@ -0,0 +1,102 @@
1
+ # NextJS Integration
2
+
3
+ ## Prisma schema
4
+
5
+ ```text
6
+ enum MediaStatusEnum {
7
+ INIT
8
+ TEMP
9
+ ACTIVE
10
+ INACTIVE
11
+ CANCELED
12
+ DELETED
13
+ }
14
+
15
+ enum MediaTypeEnum {
16
+ IMAGE
17
+ PDF
18
+ DOCS
19
+ OTHER
20
+ }
21
+
22
+ model Media {
23
+ id String @id @default(cuid())
24
+ status MediaStatusEnum @default(INIT)
25
+ type MediaTypeEnum
26
+ title String
27
+ description String?
28
+ name String
29
+ dir String?
30
+ path String
31
+ provider String
32
+ container String?
33
+ mimeType String?
34
+ size Float?
35
+ height Float?
36
+ width Float?
37
+ duration Float?
38
+ tags String[] @default([])
39
+ checksum String?
40
+ createdAt DateTime @default(now())
41
+ updatedAt DateTime @default(now()) @updatedAt
42
+ deletedAt DateTime?
43
+ }
44
+ ```
45
+
46
+ ## API endpoints
47
+
48
+ - POST: `/api/media/generate-upload-url`
49
+ Response:
50
+ ```json
51
+ {
52
+ "item": {},
53
+ "sasUrl": "<sas_upload_url>"
54
+ }
55
+ ```
56
+
57
+ - POST: `/api/media/mark-media-as-active`
58
+ Payload:
59
+ ```json
60
+ {
61
+ "mediaIds": []
62
+ }
63
+ ```
64
+ Response:
65
+ ```json
66
+ {
67
+ "items": []
68
+ }
69
+ ```
70
+
71
+ - POST: `/api/media/mark-media-as-canceled`
72
+ Payload:
73
+ ```json
74
+ {
75
+ "mediaIds": []
76
+ }
77
+ ```
78
+ Response:
79
+ ```json
80
+ {
81
+ "items": []
82
+ }
83
+ ```
84
+
85
+ - POST: `/api/media/mark-media-as-temp`
86
+ Payload:
87
+ ```json
88
+ {
89
+ "mediaIds": []
90
+ }
91
+ ```
92
+ Response:
93
+ ```json
94
+ {
95
+ "items": []
96
+ }
97
+ ```
98
+
99
+ ## Storage cleanup
100
+
101
+ Run cron and filter with status for unused media cleanup.
102
+
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # React Media Uploader
2
+
3
+ `Status: Under development`
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ bun install
9
+
10
+ bun run build
11
+ ```
12
+
13
+ ## `useMediaUploader()` hook
14
+
15
+ #### Upload on select
16
+
17
+ ```typescript
18
+ export function Uploader() {
19
+ const uploader = useMediaUploader();
20
+ return (
21
+ <input
22
+ name="image"
23
+ type="file"
24
+ multiple
25
+ onChange={uploader.onFileInputChange}
26
+ />
27
+ );
28
+ }
29
+ ```
30
+
31
+ #### Manual upload
32
+
33
+ ```typescript
34
+ export function Uploader() {
35
+ const uploader = useMediaUploader({
36
+ enableManualUpload: true,
37
+ });
38
+
39
+ const onSubmit = async (e: React.FormEvent) => {
40
+ e.preventDefault();
41
+ const mediaValues = await uploader.uploadManually();
42
+ console.log("mediaValues ->", mediaValues);
43
+ // submit data to API
44
+ };
45
+
46
+ return (
47
+ <form onSubmit={onSubmit}>
48
+ <div className="mb-4">
49
+ <input
50
+ name="image"
51
+ type="file"
52
+ multiple
53
+ onChange={uploader.onFileInputChange}
54
+ />
55
+ </div>
56
+ <div>
57
+ <button type="submit">Upload</button>
58
+ </div>
59
+ </form>
60
+ );
61
+ }
62
+ ```
package/bun.lock ADDED
@@ -0,0 +1,158 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "workspaces": {
4
+ "": {
5
+ "name": "uploader",
6
+ "devDependencies": {
7
+ "@types/react": "^18",
8
+ "@types/react-dom": "^18",
9
+ "tsc-alias": "^1",
10
+ "typescript": "^5",
11
+ },
12
+ "peerDependencies": {
13
+ "axios": ">=1",
14
+ "react": ">=18",
15
+ "react-dom": ">=18",
16
+ },
17
+ },
18
+ },
19
+ "packages": {
20
+ "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
21
+
22
+ "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
23
+
24
+ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
25
+
26
+ "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
27
+
28
+ "@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="],
29
+
30
+ "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
31
+
32
+ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
33
+
34
+ "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="],
35
+
36
+ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
37
+
38
+ "axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
39
+
40
+ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
41
+
42
+ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
43
+
44
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
45
+
46
+ "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
47
+
48
+ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
49
+
50
+ "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
51
+
52
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
53
+
54
+ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
55
+
56
+ "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
57
+
58
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
59
+
60
+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
61
+
62
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
63
+
64
+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
65
+
66
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
67
+
68
+ "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
69
+
70
+ "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
71
+
72
+ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
73
+
74
+ "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
75
+
76
+ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
77
+
78
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
79
+
80
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
81
+
82
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
83
+
84
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
85
+
86
+ "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
87
+
88
+ "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
89
+
90
+ "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="],
91
+
92
+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
93
+
94
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
95
+
96
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
97
+
98
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
99
+
100
+ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
101
+
102
+ "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
103
+
104
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
105
+
106
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
107
+
108
+ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
109
+
110
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
111
+
112
+ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
113
+
114
+ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
115
+
116
+ "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
117
+
118
+ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
119
+
120
+ "mylas": ["mylas@2.1.14", "", {}, "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog=="],
121
+
122
+ "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
123
+
124
+ "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
125
+
126
+ "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
127
+
128
+ "plimit-lit": ["plimit-lit@1.6.1", "", { "dependencies": { "queue-lit": "^1.5.1" } }, "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA=="],
129
+
130
+ "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
131
+
132
+ "queue-lit": ["queue-lit@1.5.2", "", {}, "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw=="],
133
+
134
+ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
135
+
136
+ "react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="],
137
+
138
+ "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="],
139
+
140
+ "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
141
+
142
+ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
143
+
144
+ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
145
+
146
+ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
147
+
148
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
149
+
150
+ "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
151
+
152
+ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
153
+
154
+ "tsc-alias": ["tsc-alias@1.8.16", "", { "dependencies": { "chokidar": "^3.5.3", "commander": "^9.0.0", "get-tsconfig": "^4.10.0", "globby": "^11.0.4", "mylas": "^2.1.9", "normalize-path": "^3.0.0", "plimit-lit": "^1.2.6" }, "bin": { "tsc-alias": "dist/bin/index.js" } }, "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g=="],
155
+
156
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
157
+ }
158
+ }
@@ -0,0 +1,22 @@
1
+ import { MediaStatusEnum, type MediaItem } from "../types/media";
2
+ import { type AxiosProgressEvent } from "axios";
3
+ export interface UploadMediaInfo extends Omit<AxiosProgressEvent, "event"> {
4
+ event?: undefined;
5
+ cancel?: () => Promise<void>;
6
+ }
7
+ export interface UseMediaUploaderProps<T extends object> {
8
+ defaultValues?: T;
9
+ mediaUploadSuccessStatus?: MediaStatusEnum;
10
+ enableManualUpload?: boolean;
11
+ }
12
+ export interface UseMediaUploaderResponse<T extends object> {
13
+ values: T;
14
+ setValues: (val: T) => void;
15
+ enableManualUpload?: boolean;
16
+ uploadManually: () => Promise<T>;
17
+ mediaItems: Record<string, MediaItem>;
18
+ uploadInfos: Record<string, UploadMediaInfo>;
19
+ onFileInputChange: (e: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
20
+ onFileChange: (file: File, name: string, multiple?: boolean) => Promise<void>;
21
+ }
22
+ export declare function useMediaUploader<T extends object>({ defaultValues, mediaUploadSuccessStatus, enableManualUpload, }?: UseMediaUploaderProps<T>): UseMediaUploaderResponse<T>;
@@ -0,0 +1,176 @@
1
+ import { markMediaAsCanceled, generateFileHash, generateUploadUrl, uploadToStorage, markMediaAsTemp, markMediaAsActive, generateMediaType, } from "../lib/media-helper";
2
+ import { MediaStatusEnum } from "../types/media";
3
+ import { useState } from "react";
4
+ export function useMediaUploader({ defaultValues, mediaUploadSuccessStatus = MediaStatusEnum.TEMP, enableManualUpload = false, } = {}) {
5
+ const [values, setValues] = useState(defaultValues ?? {});
6
+ const [mediaItems, setMediaItems] = useState({});
7
+ const [uploadInfos, setUploadInfos] = useState({});
8
+ const onFileInputChange = async (e) => {
9
+ console.log("onFileInputChange -> e ->", e);
10
+ const target = e.target;
11
+ const name = target.name;
12
+ const multiple = target.multiple || false;
13
+ if (target.files && target.files.length) {
14
+ for (const file of target.files) {
15
+ onFileChange(file, name, multiple);
16
+ }
17
+ }
18
+ };
19
+ const onFileChange = async (file, name, multiple = false) => {
20
+ const localId = crypto.randomUUID();
21
+ const item = {
22
+ localId,
23
+ name,
24
+ multiple,
25
+ file,
26
+ tempPreviewUrl: URL.createObjectURL(file),
27
+ media: {
28
+ type: await generateMediaType(file),
29
+ name: file.name,
30
+ mimeType: file.type,
31
+ size: file.size,
32
+ checksum: await generateFileHash(file),
33
+ },
34
+ };
35
+ setMediaItems((previous) => {
36
+ const newState = { ...previous };
37
+ newState[localId] = item;
38
+ return newState;
39
+ });
40
+ if (!enableManualUpload) {
41
+ uploadMediaFile(item);
42
+ }
43
+ };
44
+ const uploadMediaFile = async (item) => {
45
+ const sasUrlRes = await generateUploadUrl({ media: item?.media });
46
+ if (sasUrlRes?.data?.item && sasUrlRes?.data?.sasUrl) {
47
+ item["media"] = sasUrlRes?.data?.item;
48
+ setMediaItems((previous) => {
49
+ const newState = { ...previous };
50
+ newState[item.localId] = item;
51
+ return newState;
52
+ });
53
+ const abortController = new AbortController();
54
+ const uploadRes = await uploadToStorage({
55
+ sasUrl: sasUrlRes?.data?.sasUrl,
56
+ file: item.file,
57
+ onUploadProgress: (progressEvent) => {
58
+ const currentUploadInfo = {
59
+ ...progressEvent,
60
+ event: undefined,
61
+ cancel: async () => {
62
+ abortController.abort();
63
+ setUploadInfos((previous) => {
64
+ const newState = { ...previous };
65
+ if (newState[item.localId]) {
66
+ delete newState[item.localId];
67
+ }
68
+ return newState;
69
+ });
70
+ await markMediaAsCanceled({
71
+ mediaIds: [sasUrlRes?.data?.item?.id],
72
+ });
73
+ },
74
+ };
75
+ setUploadInfos((previous) => {
76
+ const newState = { ...previous };
77
+ newState[item.localId] = currentUploadInfo;
78
+ return newState;
79
+ });
80
+ },
81
+ abortController,
82
+ });
83
+ if (uploadRes?.status === 201) {
84
+ return await onMediaUploadSuccess(item);
85
+ }
86
+ else {
87
+ console.log("Media upload failed");
88
+ }
89
+ }
90
+ return undefined;
91
+ };
92
+ const onMediaUploadSuccess = async (item) => {
93
+ if (item.media.id) {
94
+ let markRes;
95
+ if (mediaUploadSuccessStatus === MediaStatusEnum.TEMP) {
96
+ markRes = await markMediaAsTemp({
97
+ mediaIds: [item.media.id],
98
+ });
99
+ }
100
+ else if (mediaUploadSuccessStatus === MediaStatusEnum.ACTIVE) {
101
+ markRes = await markMediaAsActive({
102
+ mediaIds: [item.media.id],
103
+ });
104
+ }
105
+ if (markRes?.data?.items?.length && markRes.data.items[0]) {
106
+ const newMedia = markRes.data.items[0];
107
+ item["media"] = newMedia;
108
+ setMediaItems((previous) => {
109
+ const newState = { ...previous };
110
+ newState[item.localId] = item;
111
+ return newState;
112
+ });
113
+ const currentValues = {};
114
+ if (item.multiple) {
115
+ currentValues[item.name] = [newMedia.id];
116
+ }
117
+ else {
118
+ currentValues[item.name] = newMedia.id;
119
+ }
120
+ setValues((previous) => {
121
+ const newState = { ...previous };
122
+ if (item.multiple) {
123
+ if (Array.isArray(newState[item.name])) {
124
+ newState[item.name].push(currentValues[item.name]);
125
+ }
126
+ else {
127
+ newState[item.name] = [currentValues[item.name]];
128
+ }
129
+ }
130
+ else {
131
+ newState[item.name] = currentValues[item.name];
132
+ }
133
+ return newState;
134
+ });
135
+ console.log("Media uploaded successfully");
136
+ return currentValues;
137
+ }
138
+ }
139
+ return undefined;
140
+ };
141
+ const uploadManually = async () => {
142
+ const uploadInfoIds = Object.keys(uploadInfos);
143
+ const mediaItemsToBeUploaded = Object.values(mediaItems).filter((item) => !uploadInfoIds?.includes(item.localId));
144
+ const uploadResponses = await Promise.all(mediaItemsToBeUploaded?.map(async (item) => await uploadMediaFile(item)));
145
+ console.log("uploadResponses ->", uploadResponses);
146
+ const result = { ...values };
147
+ for (const uploadResponse of uploadResponses.filter((item) => item !== undefined)) {
148
+ for (const key in uploadResponse) {
149
+ if (Array.isArray(result[key])) {
150
+ if (Array.isArray(uploadResponse[key]) &&
151
+ uploadResponse[key]?.length) {
152
+ for (const mediaId of uploadResponse[key]) {
153
+ if (!result[key]?.includes(mediaId)) {
154
+ result[key].push(mediaId);
155
+ }
156
+ }
157
+ }
158
+ }
159
+ else {
160
+ result[key] = uploadResponse[key];
161
+ }
162
+ }
163
+ }
164
+ return result;
165
+ };
166
+ return {
167
+ values,
168
+ setValues,
169
+ enableManualUpload,
170
+ uploadManually,
171
+ mediaItems,
172
+ uploadInfos,
173
+ onFileInputChange,
174
+ onFileChange,
175
+ };
176
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./hooks/use-media-uploader";
2
+ export * from "./lib/media-helper";
3
+ export * from "./types/media";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./hooks/use-media-uploader";
2
+ export * from "./lib/media-helper";
3
+ export * from "./types/media";
@@ -0,0 +1,31 @@
1
+ import { type Media, MediaTypeEnum } from "../types/media";
2
+ import { type AxiosProgressEvent } from "axios";
3
+ export declare const imageMimeTypes: string[];
4
+ export declare function generateMediaType(file: File): Promise<MediaTypeEnum>;
5
+ export declare function generateFileHash(file: File, algorithm?: string): Promise<string>;
6
+ export declare const defaultHeaders: {
7
+ "Content-Type": string;
8
+ };
9
+ export interface GenerateUploadUrlProps {
10
+ media: Partial<Media>;
11
+ }
12
+ export declare function generateUploadUrl({ media }: GenerateUploadUrlProps): Promise<import("axios").AxiosResponse<any, any, {}>>;
13
+ export interface UploadToStorageProps {
14
+ sasUrl: string;
15
+ file: File;
16
+ onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
17
+ abortController?: AbortController;
18
+ }
19
+ export declare function uploadToStorage({ sasUrl, file, onUploadProgress, abortController, }: UploadToStorageProps): Promise<import("axios").AxiosResponse<any, any, {}>>;
20
+ export interface MarkMediaAsTempProps {
21
+ mediaIds: (string | number)[];
22
+ }
23
+ export declare function markMediaAsTemp({ mediaIds }: MarkMediaAsTempProps): Promise<import("axios").AxiosResponse<any, any, {}>>;
24
+ export interface MarkMediaAsActiveProps {
25
+ mediaIds: (string | number)[];
26
+ }
27
+ export declare function markMediaAsActive({ mediaIds }: MarkMediaAsActiveProps): Promise<import("axios").AxiosResponse<any, any, {}>>;
28
+ export interface MarkMediaAsCanceledProps {
29
+ mediaIds: (string | number)[];
30
+ }
31
+ export declare function markMediaAsCanceled({ mediaIds, }: MarkMediaAsActiveProps): Promise<import("axios").AxiosResponse<any, any, {}>>;
@@ -0,0 +1,65 @@
1
+ import { MediaTypeEnum } from "../types/media";
2
+ import axios from "axios";
3
+ export const imageMimeTypes = [
4
+ "image/webp",
5
+ "image/gif",
6
+ "image/png",
7
+ "image/jpeg",
8
+ "image/jpg",
9
+ ];
10
+ export async function generateMediaType(file) {
11
+ if (imageMimeTypes.includes(file.type)) {
12
+ return MediaTypeEnum.IMAGE;
13
+ }
14
+ if (file.type === "application/pdf") {
15
+ return MediaTypeEnum.PDF;
16
+ }
17
+ return MediaTypeEnum.OTHER;
18
+ }
19
+ export async function generateFileHash(file, algorithm = "sha-1") {
20
+ const arrayBuffer = await file.arrayBuffer();
21
+ const hashBuffer = await crypto.subtle.digest(algorithm, arrayBuffer);
22
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
23
+ const hexHash = hashArray
24
+ .map((b) => b.toString(16).padStart(2, "0"))
25
+ .join("");
26
+ return hexHash;
27
+ }
28
+ export const defaultHeaders = {
29
+ "Content-Type": "application/json",
30
+ };
31
+ export async function generateUploadUrl({ media }) {
32
+ const res = await axios.post("/api/media/generate-upload-url", media, {
33
+ headers: defaultHeaders,
34
+ });
35
+ return res;
36
+ }
37
+ export async function uploadToStorage({ sasUrl, file, onUploadProgress, abortController, }) {
38
+ const res = await axios.put(sasUrl, file, {
39
+ headers: {
40
+ "x-ms-blob-type": "BlockBlob",
41
+ "Content-Type": file.type,
42
+ },
43
+ onUploadProgress: onUploadProgress,
44
+ signal: abortController ? abortController?.signal : undefined,
45
+ });
46
+ return res;
47
+ }
48
+ export async function markMediaAsTemp({ mediaIds }) {
49
+ const res = await axios.post(`/api/media/mark-media-as-temp`, { mediaIds }, {
50
+ headers: defaultHeaders,
51
+ });
52
+ return res;
53
+ }
54
+ export async function markMediaAsActive({ mediaIds }) {
55
+ const res = await axios.post(`/api/media/mark-media-as-active`, { mediaIds }, {
56
+ headers: defaultHeaders,
57
+ });
58
+ return res;
59
+ }
60
+ export async function markMediaAsCanceled({ mediaIds, }) {
61
+ const res = await axios.post(`/api/media/mark-media-as-canceled`, [mediaIds], {
62
+ headers: defaultHeaders,
63
+ });
64
+ return res;
65
+ }
@@ -0,0 +1,44 @@
1
+ export declare enum MediaTypeEnum {
2
+ IMAGE = "IMAGE",
3
+ PDF = "PDF",
4
+ DOCS = "DOCS",
5
+ OTHER = "OTHER"
6
+ }
7
+ export declare enum MediaStatusEnum {
8
+ INIT = "INIT",
9
+ TEMP = "TEMP",
10
+ ACTIVE = "ACTIVE",
11
+ INACTIVE = "INACTIVE",
12
+ CANCELED = "CANCELED",
13
+ DELETED = "DELETED"
14
+ }
15
+ export interface Media {
16
+ id: string | number;
17
+ type: MediaTypeEnum;
18
+ status: MediaStatusEnum;
19
+ title: string;
20
+ description?: string;
21
+ name: string;
22
+ dir: string;
23
+ path: string;
24
+ provider: string;
25
+ container?: string;
26
+ mimeType?: string;
27
+ size?: number;
28
+ height?: number;
29
+ width?: number;
30
+ duration?: number;
31
+ tags?: string[];
32
+ checksum?: string;
33
+ createdAt?: Date | string;
34
+ updatedAt?: Date | string;
35
+ deletedAt?: Date | string;
36
+ }
37
+ export interface MediaItem {
38
+ localId: string;
39
+ name: string;
40
+ multiple?: boolean;
41
+ file: File;
42
+ tempPreviewUrl?: string;
43
+ media: Partial<Media>;
44
+ }
@@ -0,0 +1,16 @@
1
+ export var MediaTypeEnum;
2
+ (function (MediaTypeEnum) {
3
+ MediaTypeEnum["IMAGE"] = "IMAGE";
4
+ MediaTypeEnum["PDF"] = "PDF";
5
+ MediaTypeEnum["DOCS"] = "DOCS";
6
+ MediaTypeEnum["OTHER"] = "OTHER";
7
+ })(MediaTypeEnum || (MediaTypeEnum = {}));
8
+ export var MediaStatusEnum;
9
+ (function (MediaStatusEnum) {
10
+ MediaStatusEnum["INIT"] = "INIT";
11
+ MediaStatusEnum["TEMP"] = "TEMP";
12
+ MediaStatusEnum["ACTIVE"] = "ACTIVE";
13
+ MediaStatusEnum["INACTIVE"] = "INACTIVE";
14
+ MediaStatusEnum["CANCELED"] = "CANCELED";
15
+ MediaStatusEnum["DELETED"] = "DELETED";
16
+ })(MediaStatusEnum || (MediaStatusEnum = {}));
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@susonwaiba/react-media-uploader",
3
+ "version": "0.1.0",
4
+ "source": "src/index.ts",
5
+ "module": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "main": "dist/index.js",
8
+ "scripts": {
9
+ "build": "rm -rf dist && tsc && tsc-alias"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/susonwaiba/react-media-uploader.git"
14
+ },
15
+ "author": "Suson Waiba <susonwaiba@gmail.com> (https://susonwaiba.github.io)",
16
+ "license": "MIT",
17
+ "bugs": {
18
+ "url": "https://github.com/susonwaiba/react-media-uploader/issues"
19
+ },
20
+ "homepage": "https://github.com/susonwaiba/react-media-uploader#readme",
21
+ "peerDependencies": {
22
+ "axios": ">=1",
23
+ "react": ">=18",
24
+ "react-dom": ">=18"
25
+ },
26
+ "devDependencies": {
27
+ "@types/react": "^18",
28
+ "@types/react-dom": "^18",
29
+ "tsc-alias": "^1",
30
+ "typescript": "^5"
31
+ }
32
+ }
@@ -0,0 +1,237 @@
1
+ import {
2
+ markMediaAsCanceled,
3
+ generateFileHash,
4
+ generateUploadUrl,
5
+ uploadToStorage,
6
+ markMediaAsTemp,
7
+ markMediaAsActive,
8
+ generateMediaType,
9
+ } from "@/lib/media-helper";
10
+ import { type Media, MediaStatusEnum, type MediaItem } from "@/types/media";
11
+ import { type AxiosProgressEvent } from "axios";
12
+ import { useState } from "react";
13
+
14
+ export interface UploadMediaInfo extends Omit<AxiosProgressEvent, "event"> {
15
+ event?: undefined;
16
+ cancel?: () => Promise<void>;
17
+ }
18
+
19
+ export interface UseMediaUploaderProps<T extends object> {
20
+ defaultValues?: T;
21
+ mediaUploadSuccessStatus?: MediaStatusEnum;
22
+ enableManualUpload?: boolean;
23
+ }
24
+
25
+ export interface UseMediaUploaderResponse<T extends object> {
26
+ values: T;
27
+ setValues: (val: T) => void;
28
+ enableManualUpload?: boolean;
29
+ uploadManually: () => Promise<T>;
30
+ mediaItems: Record<string, MediaItem>;
31
+ uploadInfos: Record<string, UploadMediaInfo>;
32
+ onFileInputChange: (e: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
33
+ onFileChange: (file: File, name: string, multiple?: boolean) => Promise<void>;
34
+ }
35
+
36
+ export function useMediaUploader<T extends object>({
37
+ defaultValues,
38
+ mediaUploadSuccessStatus = MediaStatusEnum.TEMP,
39
+ enableManualUpload = false,
40
+ }: UseMediaUploaderProps<T> = {}): UseMediaUploaderResponse<T> {
41
+ const [values, setValues] = useState<T>(defaultValues ?? ({} as T));
42
+ const [mediaItems, setMediaItems] = useState<Record<string, MediaItem>>({});
43
+ const [uploadInfos, setUploadInfos] = useState<
44
+ Record<string, UploadMediaInfo>
45
+ >({});
46
+
47
+ const onFileInputChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
48
+ console.log("onFileInputChange -> e ->", e);
49
+ const target = e.target as any;
50
+ const name = target.name;
51
+ const multiple = target.multiple || false;
52
+ if (target.files && target.files.length) {
53
+ for (const file of target.files) {
54
+ onFileChange(file, name, multiple);
55
+ }
56
+ }
57
+ };
58
+
59
+ const onFileChange = async (
60
+ file: File,
61
+ name: string,
62
+ multiple: boolean = false,
63
+ ) => {
64
+ const localId = crypto.randomUUID();
65
+ const item: MediaItem = {
66
+ localId,
67
+ name,
68
+ multiple,
69
+ file,
70
+ tempPreviewUrl: URL.createObjectURL(file),
71
+ media: {
72
+ type: await generateMediaType(file),
73
+ name: file.name,
74
+ mimeType: file.type,
75
+ size: file.size,
76
+ checksum: await generateFileHash(file),
77
+ },
78
+ };
79
+ setMediaItems((previous) => {
80
+ const newState = { ...previous };
81
+ newState[localId] = item;
82
+ return newState;
83
+ });
84
+ if (!enableManualUpload) {
85
+ uploadMediaFile(item);
86
+ }
87
+ };
88
+
89
+ const uploadMediaFile = async (item: MediaItem): Promise<T | undefined> => {
90
+ const sasUrlRes = await generateUploadUrl({ media: item?.media });
91
+ if (sasUrlRes?.data?.item && sasUrlRes?.data?.sasUrl) {
92
+ item["media"] = sasUrlRes?.data?.item;
93
+ setMediaItems((previous) => {
94
+ const newState = { ...previous };
95
+ newState[item.localId] = item;
96
+ return newState;
97
+ });
98
+
99
+ const abortController = new AbortController();
100
+ const uploadRes = await uploadToStorage({
101
+ sasUrl: sasUrlRes?.data?.sasUrl,
102
+ file: item.file,
103
+ onUploadProgress: (progressEvent) => {
104
+ const currentUploadInfo = {
105
+ ...progressEvent,
106
+ event: undefined,
107
+ cancel: async () => {
108
+ abortController.abort();
109
+ setUploadInfos((previous) => {
110
+ const newState = { ...previous };
111
+ if (newState[item.localId]) {
112
+ delete newState[item.localId];
113
+ }
114
+ return newState;
115
+ });
116
+ await markMediaAsCanceled({
117
+ mediaIds: [sasUrlRes?.data?.item?.id],
118
+ });
119
+ },
120
+ };
121
+ setUploadInfos((previous) => {
122
+ const newState = { ...previous };
123
+ newState[item.localId] = currentUploadInfo;
124
+ return newState;
125
+ });
126
+ },
127
+ abortController,
128
+ });
129
+ if (uploadRes?.status === 201) {
130
+ return await onMediaUploadSuccess(item);
131
+ } else {
132
+ console.log("Media upload failed");
133
+ }
134
+ }
135
+ return undefined;
136
+ };
137
+
138
+ const onMediaUploadSuccess = async (
139
+ item: MediaItem,
140
+ ): Promise<T | undefined> => {
141
+ if (item.media.id) {
142
+ let markRes:
143
+ | {
144
+ data?: {
145
+ items?: Array<Media>;
146
+ };
147
+ }
148
+ | undefined;
149
+ if (mediaUploadSuccessStatus === MediaStatusEnum.TEMP) {
150
+ markRes = await markMediaAsTemp({
151
+ mediaIds: [item.media.id],
152
+ });
153
+ } else if (mediaUploadSuccessStatus === MediaStatusEnum.ACTIVE) {
154
+ markRes = await markMediaAsActive({
155
+ mediaIds: [item.media.id],
156
+ });
157
+ }
158
+ if (markRes?.data?.items?.length && markRes.data.items[0]) {
159
+ const newMedia = markRes.data.items[0];
160
+
161
+ item["media"] = newMedia;
162
+ setMediaItems((previous) => {
163
+ const newState = { ...previous };
164
+ newState[item.localId] = item;
165
+ return newState;
166
+ });
167
+
168
+ const currentValues: any = {};
169
+ if (item.multiple) {
170
+ currentValues[item.name] = [newMedia.id];
171
+ } else {
172
+ currentValues[item.name] = newMedia.id;
173
+ }
174
+ setValues((previous: T) => {
175
+ const newState: any = { ...previous };
176
+ if (item.multiple) {
177
+ if (Array.isArray(newState[item.name])) {
178
+ newState[item.name].push(currentValues[item.name]);
179
+ } else {
180
+ newState[item.name] = [currentValues[item.name]];
181
+ }
182
+ } else {
183
+ newState[item.name] = currentValues[item.name];
184
+ }
185
+ return newState;
186
+ });
187
+ console.log("Media uploaded successfully");
188
+ return currentValues;
189
+ }
190
+ }
191
+ return undefined;
192
+ };
193
+
194
+ const uploadManually = async () => {
195
+ const uploadInfoIds = Object.keys(uploadInfos);
196
+ const mediaItemsToBeUploaded = Object.values(mediaItems).filter(
197
+ (item) => !uploadInfoIds?.includes(item.localId),
198
+ );
199
+ const uploadResponses = await Promise.all(
200
+ mediaItemsToBeUploaded?.map(async (item) => await uploadMediaFile(item)),
201
+ );
202
+ console.log("uploadResponses ->", uploadResponses);
203
+ const result: any = { ...values };
204
+ for (const uploadResponse of uploadResponses.filter(
205
+ (item) => item !== undefined,
206
+ )) {
207
+ for (const key in uploadResponse) {
208
+ if (Array.isArray(result[key])) {
209
+ if (
210
+ Array.isArray(uploadResponse[key]) &&
211
+ uploadResponse[key]?.length
212
+ ) {
213
+ for (const mediaId of uploadResponse[key]) {
214
+ if (!result[key]?.includes(mediaId)) {
215
+ result[key].push(mediaId);
216
+ }
217
+ }
218
+ }
219
+ } else {
220
+ result[key] = uploadResponse[key];
221
+ }
222
+ }
223
+ }
224
+ return result;
225
+ };
226
+
227
+ return {
228
+ values,
229
+ setValues,
230
+ enableManualUpload,
231
+ uploadManually,
232
+ mediaItems,
233
+ uploadInfos,
234
+ onFileInputChange,
235
+ onFileChange,
236
+ };
237
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "@/hooks/use-media-uploader";
2
+ export * from "@/lib/media-helper";
3
+ export * from "@/types/media";
@@ -0,0 +1,119 @@
1
+ import { type Media, MediaTypeEnum } from "@/types/media";
2
+ import axios, { type AxiosProgressEvent } from "axios";
3
+
4
+ export const imageMimeTypes = [
5
+ "image/webp",
6
+ "image/gif",
7
+ "image/png",
8
+ "image/jpeg",
9
+ "image/jpg",
10
+ ];
11
+
12
+ export async function generateMediaType(file: File): Promise<MediaTypeEnum> {
13
+ if (imageMimeTypes.includes(file.type)) {
14
+ return MediaTypeEnum.IMAGE;
15
+ }
16
+ if (file.type === "application/pdf") {
17
+ return MediaTypeEnum.PDF;
18
+ }
19
+ return MediaTypeEnum.OTHER;
20
+ }
21
+
22
+ export async function generateFileHash(
23
+ file: File,
24
+ algorithm = "sha-1",
25
+ ): Promise<string> {
26
+ const arrayBuffer = await file.arrayBuffer();
27
+ const hashBuffer = await crypto.subtle.digest(algorithm, arrayBuffer);
28
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
29
+ const hexHash = hashArray
30
+ .map((b) => b.toString(16).padStart(2, "0"))
31
+ .join("");
32
+ return hexHash;
33
+ }
34
+
35
+ export const defaultHeaders = {
36
+ "Content-Type": "application/json",
37
+ };
38
+
39
+ export interface GenerateUploadUrlProps {
40
+ media: Partial<Media>;
41
+ }
42
+
43
+ export async function generateUploadUrl({ media }: GenerateUploadUrlProps) {
44
+ const res = await axios.post("/api/media/generate-upload-url", media, {
45
+ headers: defaultHeaders,
46
+ });
47
+ return res;
48
+ }
49
+
50
+ export interface UploadToStorageProps {
51
+ sasUrl: string;
52
+ file: File;
53
+ onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
54
+ abortController?: AbortController;
55
+ }
56
+
57
+ export async function uploadToStorage({
58
+ sasUrl,
59
+ file,
60
+ onUploadProgress,
61
+ abortController,
62
+ }: UploadToStorageProps) {
63
+ const res = await axios.put(sasUrl, file, {
64
+ headers: {
65
+ "x-ms-blob-type": "BlockBlob",
66
+ "Content-Type": file.type,
67
+ },
68
+ onUploadProgress: onUploadProgress,
69
+ signal: abortController ? abortController?.signal : undefined,
70
+ });
71
+ return res;
72
+ }
73
+
74
+ export interface MarkMediaAsTempProps {
75
+ mediaIds: (string | number)[];
76
+ }
77
+
78
+ export async function markMediaAsTemp({ mediaIds }: MarkMediaAsTempProps) {
79
+ const res = await axios.post(
80
+ `/api/media/mark-media-as-temp`,
81
+ { mediaIds },
82
+ {
83
+ headers: defaultHeaders,
84
+ },
85
+ );
86
+ return res;
87
+ }
88
+
89
+ export interface MarkMediaAsActiveProps {
90
+ mediaIds: (string | number)[];
91
+ }
92
+
93
+ export async function markMediaAsActive({ mediaIds }: MarkMediaAsActiveProps) {
94
+ const res = await axios.post(
95
+ `/api/media/mark-media-as-active`,
96
+ { mediaIds },
97
+ {
98
+ headers: defaultHeaders,
99
+ },
100
+ );
101
+ return res;
102
+ }
103
+
104
+ export interface MarkMediaAsCanceledProps {
105
+ mediaIds: (string | number)[];
106
+ }
107
+
108
+ export async function markMediaAsCanceled({
109
+ mediaIds,
110
+ }: MarkMediaAsActiveProps) {
111
+ const res = await axios.post(
112
+ `/api/media/mark-media-as-canceled`,
113
+ [mediaIds],
114
+ {
115
+ headers: defaultHeaders,
116
+ },
117
+ );
118
+ return res;
119
+ }
@@ -0,0 +1,47 @@
1
+ export enum MediaTypeEnum {
2
+ IMAGE = "IMAGE",
3
+ PDF = "PDF",
4
+ DOCS = "DOCS",
5
+ OTHER = "OTHER",
6
+ }
7
+
8
+ export enum MediaStatusEnum {
9
+ INIT = "INIT",
10
+ TEMP = "TEMP",
11
+ ACTIVE = "ACTIVE",
12
+ INACTIVE = "INACTIVE",
13
+ CANCELED = "CANCELED",
14
+ DELETED = "DELETED",
15
+ }
16
+
17
+ export interface Media {
18
+ id: string | number;
19
+ type: MediaTypeEnum;
20
+ status: MediaStatusEnum;
21
+ title: string;
22
+ description?: string;
23
+ name: string;
24
+ dir: string;
25
+ path: string;
26
+ provider: string;
27
+ container?: string;
28
+ mimeType?: string;
29
+ size?: number;
30
+ height?: number;
31
+ width?: number;
32
+ duration?: number;
33
+ tags?: string[];
34
+ checksum?: string;
35
+ createdAt?: Date | string;
36
+ updatedAt?: Date | string;
37
+ deletedAt?: Date | string;
38
+ }
39
+
40
+ export interface MediaItem {
41
+ localId: string;
42
+ name: string;
43
+ multiple?: boolean;
44
+ file: File;
45
+ tempPreviewUrl?: string;
46
+ media: Partial<Media>;
47
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "jsx": "react-jsx",
6
+ "declaration": true,
7
+ "emitDeclarationOnly": false,
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "moduleResolution": "Bundler",
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "paths": {
14
+ "@/*": ["./src/*"]
15
+ }
16
+ },
17
+ "include": ["src"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }