├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── npm-publish.yml │ └── test.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .lintstagedrc ├── .prettierignore ├── .prettierrc.js ├── .storybook ├── main.js └── preview.jsx ├── LICENSE ├── README.md ├── examples └── next │ ├── .gitignore │ ├── README.md │ ├── next.config.js │ ├── package.json │ ├── pnpm-lock.yaml │ ├── postcss.config.js │ ├── public │ ├── next.svg │ └── vercel.svg │ ├── src │ └── app │ │ ├── components │ │ ├── BasicButton │ │ │ ├── BasicButton.tsx │ │ │ └── index.ts │ │ ├── Loading │ │ │ ├── Loading.tsx │ │ │ └── index.ts │ │ ├── ProgressBar │ │ │ ├── ProgressBar.tsx │ │ │ └── index.ts │ │ ├── UploadIcon │ │ │ ├── UploadIcon.tsx │ │ │ └── index.ts │ │ └── Uploader.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── tailwind.config.ts │ └── tsconfig.json ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── rollup.config.js ├── setupTests.js ├── src ├── TusClientProvider │ ├── TusClientProvider.tsx │ ├── constants.ts │ ├── index.ts │ ├── store │ │ ├── contexts.ts │ │ ├── tucClientActions.ts │ │ └── tusClientReducer.ts │ └── types.ts ├── __stories__ │ ├── Basic.stories.tsx │ ├── CacheKey.stories.tsx │ ├── DefaultOptions.stories.tsx │ ├── components │ │ ├── BasicButton │ │ │ ├── BasicButton.tsx │ │ │ └── index.ts │ │ ├── LoadingCircle │ │ │ ├── LoadingCircle.tsx │ │ │ └── index.ts │ │ ├── ProgressBar │ │ │ ├── ProgressBar.tsx │ │ │ └── index.ts │ │ └── UploadIcon │ │ │ ├── UploadIcon.tsx │ │ │ └── index.ts │ ├── constants.ts │ ├── global.css │ └── tailwind.config.js ├── __tests__ │ ├── TusClientProvider.test.tsx │ ├── createUpload.test.ts │ ├── useTus.test.tsx │ ├── useTusStore.test.tsx │ └── utils │ │ ├── createMock.ts │ │ ├── getBlob.ts │ │ ├── getDefaultOptions.ts │ │ └── mock.ts ├── index.ts ├── types.ts ├── useTus │ ├── index.ts │ └── useTus.ts ├── useTusClient │ ├── index.ts │ └── useTusClient.ts ├── useTusStore │ ├── index.ts │ └── useTusStore.ts └── utils │ ├── core │ ├── createUpload.ts │ ├── index.ts │ ├── splitTusHooksUploadOptions.ts │ └── startOrResumeUpload.ts │ ├── index.ts │ └── options │ ├── index.ts │ ├── mergeUseTusOptions.ts │ └── useAutoAbort.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | .next 2 | dist 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | jest: true, 7 | }, 8 | plugins: ["react", "react-hooks", "@typescript-eslint", "prettier"], 9 | extends: [ 10 | "airbnb", 11 | "plugin:react/recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:import/typescript", 14 | "plugin:react-hooks/recommended", 15 | "prettier", 16 | "plugin:storybook/recommended", 17 | ], 18 | globals: { 19 | Atomics: "readonly", 20 | SharedArrayBuffer: "readonly", 21 | }, 22 | parser: "@typescript-eslint/parser", 23 | parserOptions: { 24 | ecmaFeatures: { 25 | jsx: true, 26 | }, 27 | ecmaVersion: 2019, 28 | sourceType: "module", 29 | }, 30 | rules: { 31 | "no-use-before-define": 0, 32 | "no-shadow": 0, 33 | "no-underscore-dangle": "off", 34 | "@typescript-eslint/explicit-function-return-type": 0, 35 | "@typescript-eslint/explicit-module-boundary-types": 0, 36 | "@typescript-eslint/no-var-requires": 0, 37 | "react/jsx-uses-react": "off", 38 | "react/react-in-jsx-scope": "off", 39 | "react/prop-types": "off", 40 | "react/display-name": "off", 41 | "react/function-component-definition": "off", 42 | "react/require-default-props": "off", 43 | "import/extensions": [ 44 | "error", 45 | "always", 46 | { ts: "never", tsx: "never", js: "never" }, 47 | ], 48 | "import/prefer-default-export": "off", 49 | "import/no-extraneous-dependencies": [ 50 | "error", 51 | { 52 | devDependencies: true, 53 | optionalDependencies: false, 54 | }, 55 | ], 56 | "react/jsx-filename-extension": [ 57 | "error", 58 | { 59 | extensions: [".jsx", ".tsx"], 60 | }, 61 | ], 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Automatic release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - "!*" 8 | 9 | jobs: 10 | check-version: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: 20 17 | - name: Install pnpm 18 | uses: pnpm/action-setup@v2 19 | id: pnpm-install 20 | with: 21 | version: 8.8.0 22 | run_install: false 23 | - name: Install dependencies 24 | shell: bash 25 | run: pnpm install --frozen-lockfile 26 | - run: pnpm add can-npm-publish 27 | - run: npx can-npm-publish --verbose 28 | env: 29 | NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLISH_TOKEN}} 30 | test: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v2 34 | - uses: actions/setup-node@v1 35 | with: 36 | node-version: 20 37 | - name: Install pnpm 38 | uses: pnpm/action-setup@v2 39 | id: pnpm-install 40 | with: 41 | version: 8.8.0 42 | run_install: false 43 | - name: Install dependencies 44 | shell: bash 45 | run: pnpm install --frozen-lockfile 46 | - run: pnpm install --frozen-lockfile 47 | - run: pnpm test 48 | 49 | publish-npm: 50 | needs: [ test, check-version ] 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v2 54 | - uses: actions/setup-node@v1 55 | with: 56 | node-version: 20 57 | registry-url: https://registry.npmjs.org/ 58 | - name: Install pnpm 59 | uses: pnpm/action-setup@v2 60 | id: pnpm-install 61 | with: 62 | version: 8.8.0 63 | run_install: false 64 | - name: Install dependencies 65 | shell: bash 66 | run: pnpm install --frozen-lockfile 67 | - run: pnpm install --frozen-lockfile 68 | - run: pnpm publish 69 | env: 70 | NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLISH_TOKEN}} 71 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [16.x, 18.x, 20.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install pnpm 25 | uses: pnpm/action-setup@v2 26 | id: pnpm-install 27 | with: 28 | version: 8.8.0 29 | run_install: false 30 | - run: pnpm install --frozen-lockfile 31 | - run: pnpm build 32 | - run: pnpm test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /storybook-static 2 | /dist 3 | .DS_Store 4 | 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | .yarn 13 | lerna-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | 84 | # Next.js build output 85 | .next 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and not Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | 112 | # Stores VSCode versions used for testing VSCode extensions 113 | .vscode-test 114 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,tsx,js,jsx,json}": [ 3 | "prettier --write", 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | **/.next/* 3 | **/storybook-static/* 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: false, 6 | }; 7 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 3 | addons: ["@storybook/addon-links", "@storybook/addon-essentials"], 4 | framework: { 5 | name: "@storybook/react-vite", 6 | options: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.storybook/preview.jsx: -------------------------------------------------------------------------------- 1 | import "../src/__stories__/global.css"; 2 | 3 | const GithubLink = () => ( 4 | 8 | 14 | 15 | 16 | 17 | ); 18 | const Providers = (Story, context) => { 19 | return ( 20 |
21 | 22 |
23 | 24 |
25 |
26 | ); 27 | }; 28 | 29 | export const decorators = [Providers]; 30 | export const parameters = { 31 | actions: { argTypesRegex: "^on[A-Z].*" }, 32 | controls: { 33 | matchers: { 34 | color: /(background|color)$/i, 35 | date: /Date$/, 36 | }, 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 kqito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | use-tus 3 |

4 | 5 |

6 | React hooks for resumable file uploads using tus. 7 |

8 | 9 |

10 | Build status 11 | Npm version 12 | License 13 |

14 | 15 | ## Features 16 | - Resumable file uploads in React. 17 | - Improved file upload management within the React component lifecycle. 18 | - Lightweight and simple interface hooks. 19 | - Manage [Upload](https://github.com/tus/tus-js-client/blob/master/docs/api.md#tusuploadfile-options) instances via context. 20 | - TypeScript support. 21 | 22 | ## Demo 23 | Try out the [use-tus demo](https://kqito.github.io/use-tus/?path=/story/usetus--basic). 24 | 25 | 26 | ## Installation 27 | Install the package using your package manager of choice. 28 | 29 | ```sh 30 | npm install use-tus tus-js-client 31 | ``` 32 | 33 | ## Usage 34 | Below is an example of how to use `useTus`. 35 | 36 | ```tsx 37 | import { useTus } from 'use-tus' 38 | 39 | const Uploader = () => { 40 | const { upload, setUpload, isSuccess, error, remove } = useTus(); 41 | 42 | const handleSetUpload = useCallback((event: ChangeEvent) => { 43 | const file = event.target.files.item(0); 44 | if (!file) return; 45 | 46 | setUpload(file, { 47 | endpoint: 'https://tusd.tusdemo.net/files/', 48 | metadata: { 49 | filename: file.name, 50 | filetype: file.type, 51 | }, 52 | }); 53 | }, [setUpload]); 54 | 55 | const handleStart = useCallback(() => { 56 | if (upload) upload.start(); 57 | }, [upload]); 58 | 59 | return ( 60 |
61 | 62 | 63 |
64 | ); 65 | }; 66 | ``` 67 | 68 | 69 | ## API 70 | ### `useTus` hooks 71 | 72 | ```tsx 73 | const { upload, setUpload, isSuccess, isAborted, isUploading, error, remove } = useTus({ autoAbort, autoStart, uploadOptions, Upload }); 74 | ``` 75 | 76 | `useTus` is a hooks that creates an object to perform Resumable file upload. 77 | 78 | ### Arguments 79 | - `autoAbort` (type: `boolean | undefined`) (default: true) 80 | - Whether or not to automatically abort uploads when useTus hooks is unmounted. 81 | 82 | - `autoStart` (type: `boolean | undefined`) (default: false) 83 | - Whether or not to start upload the file after `setUpload` function. 84 | 85 | - `uploadOptions` (type: `TusHooksUploadFnOptions | undefined`) (default: undefined) 86 | - Option to used by upload object that generated by that hooks. 87 | 88 | - `Upload` (type: `Upload | undefined`) (default: undefined) 89 | - Option to specify customized own Upload class with this hooks. 90 | 91 | #### `uploadOptions` 92 | This option extends the `UploadOptions` provided by `tus-js-client`, allowing every callback to receive the upload instance as the final argument. For detailed type information on `UploadOptions`, see [here](https://github.com/tus/tus-js-client/blob/master/lib/index.d.ts#L22). 93 | 94 | Example: 95 | 96 | ```ts 97 | setUpload(file, { 98 | onSuccess: (upload) => { 99 | console.log(upload.url) 100 | }, 101 | onError: (error, upload) => { 102 | console.log(error) 103 | setTimeout(() => { 104 | upload.start() 105 | }, 1000) 106 | } 107 | }); 108 | ``` 109 | 110 | 111 | #### Returns 112 | - `upload` (type: `tus.Upload | undefined`) 113 | - Used for resumable file uploads. Undefined unless `setUpload` is called. 114 | - For detailed usage, see [here](https://github.com/tus/tus-js-client#example). 115 | 116 | - `setUpload` (type: `(file: tus.Upload['file'], options?: TusHooksUploadFnOptions) => void`) 117 | - Function to create an `Upload`. `uploadOptions` properties are overwritten if `options` is specified. 118 | 119 | - `isSuccess` (type: `boolean`) 120 | - Indicates if the upload was successful. 121 | 122 | - `isAborted` (type: `boolean`) 123 | - Indicates if the upload was aborted. 124 | 125 | - `isUploading` (type: `boolean`) 126 | - Indicates if an upload is in progress. 127 | 128 | - `error` (type: `Error | undefined`) 129 | - Error when the upload fails. 130 | 131 | - `remove` (type: `() => void`) 132 | - Function to reset states. 133 | 134 | ### `useTusStore` hooks 135 | 136 | ```tsx 137 | const { upload, setUpload, isSuccess, isAborted, isUploading, error, remove } = useTusStore(cacheKey, { autoAbort, autoStart, uploadOptions, Upload }); 138 | ``` 139 | 140 | `useTusStore` creates an object for resumable file uploads and stores it in a context. This is useful for handling uploads across components or pages. 141 | 142 | > [!NOTE] 143 | > `useTusStore` requires `TusClientProvider` as a parent or higher element. 144 | 145 | #### Arguments 146 | - `cacheKey` (type: `string`) 147 | - Key associated with the `Upload` object created by `setUpload`. 148 | 149 | - `autoAbort` (type: `boolean | undefined`, default: `true`) 150 | - Automatically abort uploads when `useTusStore` is unmounted. 151 | 152 | - `autoStart` (type: `boolean | undefined`, default: `false`) 153 | - Automatically start the upload after calling the `setUpload` function. 154 | 155 | - `uploadOptions` (type: `TusHooksUploadFnOptions | undefined`, default: `undefined`) 156 | - Set options to be used by the upload object generated by this hook. 157 | 158 | - `Upload` (type: `Upload | undefined`, default: `undefined`) 159 | - Specify a customized `Upload` class with this hook. 160 | 161 | #### Returns 162 | - `upload` (type: `tus.Upload | undefined`) 163 | - Used for resumable file uploads. Undefined unless `setUpload` is called. 164 | - Corresponds to the `Upload` associated with `cacheKey` in `TusClientProvider`. 165 | 166 | - `setUpload` (type: `(file: tus.Upload['file'], options?: TusHooksUploadFnOptions) => void`) 167 | - Function to create an `Upload`. `uploadOptions` properties are overwritten if `options` is specified. 168 | 169 | - `isSuccess` (type: `boolean`) 170 | - Indicates if the upload was successful. 171 | 172 | - `isAborted` (type: `boolean`) 173 | - Indicates if the upload was aborted. 174 | 175 | - `isUploading` (type: `boolean`) 176 | - Indicates if an upload is in progress. 177 | 178 | - `error` (type: `Error | undefined`) 179 | - Error when the upload fails. 180 | 181 | - `remove` (type: `() => void`) 182 | - Function to delete the `Upload` associated with `cacheKey`. 183 | 184 | ### `TusClientProvider` 185 | 186 | ```tsx 187 | () => ( 188 | 189 | {children} 190 | 191 | ) 192 | ``` 193 | 194 | `TusClientProvider` stores `Upload` objects created with `useTusStore`. 195 | 196 | #### Props 197 | - `defaultOptions` (type: `(file: tus.Upload['file']) => TusHooksUploadFnOptions | undefined`) 198 | - Default options object used when creating new uploads. For more details, see [here](https://github.com/tus/tus-js-client/blob/master/docs/api.md#tusdefaultoptions). 199 | 200 | ### `useTusClient` 201 | 202 | ```tsx 203 | const { state, removeUpload, reset } = useTusClient(); 204 | ``` 205 | 206 | `useTusClient` retrieves and resets the state of `TusClientProvider`. 207 | 208 | #### Returns 209 | - `state` (type: `{ [cacheKey: string]: UploadState | undefined }`) 210 | - Upload information associated with `cacheKey`. 211 | 212 | - `removeUpload` (type: `(cacheKey: string) => void`) 213 | - Remove the upload instance associated with `cacheKey`. 214 | 215 | - `reset` (type: `() => void`) 216 | - Initialize the value of `TusClientProvider`. 217 | 218 | ## Examples 219 | Here are some examples of how to use `use-tus`. 220 | 221 | ### Uploading a file 222 | Use `setUpload` and `upload.start` functions to perform resumable file uploads. 223 | 224 | ```tsx 225 | import { useTus } from 'use-tus' 226 | 227 | const Uploader = () => { 228 | const { upload, setUpload } = useTus(); 229 | 230 | const handleSetUpload = useCallback((event: ChangeEvent) => { 231 | const file = event.target.files.item(0); 232 | if (!file) return; 233 | 234 | setUpload(file, { 235 | endpoint: 'https://tusd.tusdemo.net/files/', 236 | metadata: { 237 | filename: file.name, 238 | filetype: file.type, 239 | }, 240 | }); 241 | }, [setUpload]); 242 | 243 | const handleStart = useCallback(() => { 244 | if (upload) upload.start(); 245 | }, [upload]); 246 | 247 | return ( 248 |
249 | 250 | 251 |
252 | ); 253 | }; 254 | ``` 255 | 256 | > [!TIP] 257 | > You can also set `autoStart` to automatically start uploading files after `setUpload` is called. 258 | 259 | ```tsx 260 | import { useTus } from 'use-tus' 261 | 262 | const Uploader = () => { 263 | const { upload, setUpload } = useTus({ autoStart: true }); 264 | 265 | const handleSetUpload = useCallback((event: ChangeEvent) => { 266 | const file = event.target.files.item(0); 267 | if (!file) return; 268 | 269 | setUpload(file, { 270 | endpoint: 'https://tusd.tusdemo.net/files/', 271 | metadata: { 272 | filename: file.name, 273 | filetype: file.type, 274 | }, 275 | }); 276 | }, [setUpload]); 277 | 278 | return ( 279 | 280 | ); 281 | }; 282 | ``` 283 | 284 | ### Aborting a File Upload 285 | Use the `upload.abort` function to abort an upload. 286 | 287 | ```tsx 288 | import { useTus } from 'use-tus' 289 | 290 | const Aborter = () => { 291 | const { upload } = useTus(); 292 | 293 | const handleAbort = useCallback(() => { 294 | if (upload) upload.abort(); 295 | }, [upload]); 296 | 297 | return ( 298 |
299 | 300 |
301 | ); 302 | }; 303 | ``` 304 | 305 | ### Default Options for Upload 306 | Specify default options in `defaultOptions` props of the `TusClientProvider`. 307 | 308 | ```tsx 309 | import { useTusStore, DefaultOptions, TusClientProvider } from 'use-tus' 310 | 311 | const defaultOptions: DefaultOptions = (file) => ({ 312 | endpoint: 'https://tusd.tusdemo.net/files/', 313 | metadata: 314 | file instanceof File 315 | ? { 316 | filename: file.name, 317 | filetype: file.type, 318 | } 319 | : undefined, 320 | }); 321 | 322 | const App = () => ( 323 | 324 | 325 | 326 | ); 327 | 328 | const Uploader = () => { 329 | const { setUpload } = useTusStore('cacheKey', { autoStart: true }); 330 | 331 | const handleSetUpload = useCallback((event: ChangeEvent) => { 332 | const file = event.target.files.item(0); 333 | if (!file) return; 334 | 335 | // Uploads the selected file using default options. 336 | // Overrides if options are provided to setUpload. 337 | setUpload(file); 338 | }, [setUpload]); 339 | 340 | return ; 341 | }; 342 | ``` 343 | 344 | ### Specify Upload Key 345 | Specify `cacheKey` to associate uploads across components/pages. 346 | 347 | ```tsx 348 | import { useTusStore } from 'use-tus' 349 | 350 | const SelectFileComponent = (file: File) => { 351 | const { setUpload } = useTusStore('upload-thumbnail'); 352 | setUpload(file); 353 | }; 354 | 355 | const UploadFileComponent = () => { 356 | const { upload } = useTusStore('upload-thumbnail'); 357 | if (upload) upload.start(); 358 | }; 359 | ``` 360 | 361 | ## License 362 | [MIT © kqito](./LICENSE) 363 | -------------------------------------------------------------------------------- /examples/next/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /examples/next/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/next/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /examples/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "clsx": "^2.0.0", 13 | "next": "13.5.6", 14 | "react": "^18", 15 | "react-dom": "^18", 16 | "tus-js-client": "^3.1.1", 17 | "use-tus": "latest" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20", 21 | "@types/react": "^18", 22 | "@types/react-dom": "^18", 23 | "autoprefixer": "^10", 24 | "postcss": "^8", 25 | "prettier": "^3.0.3", 26 | "tailwindcss": "^3", 27 | "typescript": "^5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/next/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | clsx: 9 | specifier: ^2.0.0 10 | version: 2.0.0 11 | next: 12 | specifier: 13.5.6 13 | version: 13.5.6(react-dom@18.2.0)(react@18.2.0) 14 | react: 15 | specifier: ^18 16 | version: 18.2.0 17 | react-dom: 18 | specifier: ^18 19 | version: 18.2.0(react@18.2.0) 20 | tus-js-client: 21 | specifier: ^3.1.1 22 | version: 3.1.1 23 | use-tus: 24 | specifier: latest 25 | version: 0.7.3(react@18.2.0)(tus-js-client@3.1.1) 26 | 27 | devDependencies: 28 | '@types/node': 29 | specifier: ^20 30 | version: 20.8.7 31 | '@types/react': 32 | specifier: ^18 33 | version: 18.2.31 34 | '@types/react-dom': 35 | specifier: ^18 36 | version: 18.2.14 37 | autoprefixer: 38 | specifier: ^10 39 | version: 10.4.16(postcss@8.4.31) 40 | postcss: 41 | specifier: ^8 42 | version: 8.4.31 43 | prettier: 44 | specifier: ^3.0.3 45 | version: 3.0.3 46 | tailwindcss: 47 | specifier: ^3 48 | version: 3.3.3 49 | typescript: 50 | specifier: ^5 51 | version: 5.2.2 52 | 53 | packages: 54 | 55 | /@alloc/quick-lru@5.2.0: 56 | resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} 57 | engines: {node: '>=10'} 58 | dev: true 59 | 60 | /@jridgewell/gen-mapping@0.3.3: 61 | resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} 62 | engines: {node: '>=6.0.0'} 63 | dependencies: 64 | '@jridgewell/set-array': 1.1.2 65 | '@jridgewell/sourcemap-codec': 1.4.15 66 | '@jridgewell/trace-mapping': 0.3.20 67 | dev: true 68 | 69 | /@jridgewell/resolve-uri@3.1.1: 70 | resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} 71 | engines: {node: '>=6.0.0'} 72 | dev: true 73 | 74 | /@jridgewell/set-array@1.1.2: 75 | resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} 76 | engines: {node: '>=6.0.0'} 77 | dev: true 78 | 79 | /@jridgewell/sourcemap-codec@1.4.15: 80 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 81 | dev: true 82 | 83 | /@jridgewell/trace-mapping@0.3.20: 84 | resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} 85 | dependencies: 86 | '@jridgewell/resolve-uri': 3.1.1 87 | '@jridgewell/sourcemap-codec': 1.4.15 88 | dev: true 89 | 90 | /@next/env@13.5.6: 91 | resolution: {integrity: sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==} 92 | dev: false 93 | 94 | /@next/swc-darwin-arm64@13.5.6: 95 | resolution: {integrity: sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==} 96 | engines: {node: '>= 10'} 97 | cpu: [arm64] 98 | os: [darwin] 99 | requiresBuild: true 100 | dev: false 101 | optional: true 102 | 103 | /@next/swc-darwin-x64@13.5.6: 104 | resolution: {integrity: sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==} 105 | engines: {node: '>= 10'} 106 | cpu: [x64] 107 | os: [darwin] 108 | requiresBuild: true 109 | dev: false 110 | optional: true 111 | 112 | /@next/swc-linux-arm64-gnu@13.5.6: 113 | resolution: {integrity: sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==} 114 | engines: {node: '>= 10'} 115 | cpu: [arm64] 116 | os: [linux] 117 | requiresBuild: true 118 | dev: false 119 | optional: true 120 | 121 | /@next/swc-linux-arm64-musl@13.5.6: 122 | resolution: {integrity: sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==} 123 | engines: {node: '>= 10'} 124 | cpu: [arm64] 125 | os: [linux] 126 | requiresBuild: true 127 | dev: false 128 | optional: true 129 | 130 | /@next/swc-linux-x64-gnu@13.5.6: 131 | resolution: {integrity: sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==} 132 | engines: {node: '>= 10'} 133 | cpu: [x64] 134 | os: [linux] 135 | requiresBuild: true 136 | dev: false 137 | optional: true 138 | 139 | /@next/swc-linux-x64-musl@13.5.6: 140 | resolution: {integrity: sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==} 141 | engines: {node: '>= 10'} 142 | cpu: [x64] 143 | os: [linux] 144 | requiresBuild: true 145 | dev: false 146 | optional: true 147 | 148 | /@next/swc-win32-arm64-msvc@13.5.6: 149 | resolution: {integrity: sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==} 150 | engines: {node: '>= 10'} 151 | cpu: [arm64] 152 | os: [win32] 153 | requiresBuild: true 154 | dev: false 155 | optional: true 156 | 157 | /@next/swc-win32-ia32-msvc@13.5.6: 158 | resolution: {integrity: sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==} 159 | engines: {node: '>= 10'} 160 | cpu: [ia32] 161 | os: [win32] 162 | requiresBuild: true 163 | dev: false 164 | optional: true 165 | 166 | /@next/swc-win32-x64-msvc@13.5.6: 167 | resolution: {integrity: sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==} 168 | engines: {node: '>= 10'} 169 | cpu: [x64] 170 | os: [win32] 171 | requiresBuild: true 172 | dev: false 173 | optional: true 174 | 175 | /@nodelib/fs.scandir@2.1.5: 176 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 177 | engines: {node: '>= 8'} 178 | dependencies: 179 | '@nodelib/fs.stat': 2.0.5 180 | run-parallel: 1.2.0 181 | dev: true 182 | 183 | /@nodelib/fs.stat@2.0.5: 184 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 185 | engines: {node: '>= 8'} 186 | dev: true 187 | 188 | /@nodelib/fs.walk@1.2.8: 189 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 190 | engines: {node: '>= 8'} 191 | dependencies: 192 | '@nodelib/fs.scandir': 2.1.5 193 | fastq: 1.15.0 194 | dev: true 195 | 196 | /@swc/helpers@0.5.2: 197 | resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} 198 | dependencies: 199 | tslib: 2.6.2 200 | dev: false 201 | 202 | /@types/node@20.8.7: 203 | resolution: {integrity: sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==} 204 | dependencies: 205 | undici-types: 5.25.3 206 | dev: true 207 | 208 | /@types/prop-types@15.7.9: 209 | resolution: {integrity: sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==} 210 | dev: true 211 | 212 | /@types/react-dom@18.2.14: 213 | resolution: {integrity: sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==} 214 | dependencies: 215 | '@types/react': 18.2.31 216 | dev: true 217 | 218 | /@types/react@18.2.31: 219 | resolution: {integrity: sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==} 220 | dependencies: 221 | '@types/prop-types': 15.7.9 222 | '@types/scheduler': 0.16.5 223 | csstype: 3.1.2 224 | dev: true 225 | 226 | /@types/scheduler@0.16.5: 227 | resolution: {integrity: sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==} 228 | dev: true 229 | 230 | /any-promise@1.3.0: 231 | resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 232 | dev: true 233 | 234 | /anymatch@3.1.3: 235 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 236 | engines: {node: '>= 8'} 237 | dependencies: 238 | normalize-path: 3.0.0 239 | picomatch: 2.3.1 240 | dev: true 241 | 242 | /arg@5.0.2: 243 | resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} 244 | dev: true 245 | 246 | /autoprefixer@10.4.16(postcss@8.4.31): 247 | resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==} 248 | engines: {node: ^10 || ^12 || >=14} 249 | hasBin: true 250 | peerDependencies: 251 | postcss: ^8.1.0 252 | dependencies: 253 | browserslist: 4.22.1 254 | caniuse-lite: 1.0.30001551 255 | fraction.js: 4.3.7 256 | normalize-range: 0.1.2 257 | picocolors: 1.0.0 258 | postcss: 8.4.31 259 | postcss-value-parser: 4.2.0 260 | dev: true 261 | 262 | /balanced-match@1.0.2: 263 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 264 | dev: true 265 | 266 | /binary-extensions@2.2.0: 267 | resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} 268 | engines: {node: '>=8'} 269 | dev: true 270 | 271 | /brace-expansion@1.1.11: 272 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 273 | dependencies: 274 | balanced-match: 1.0.2 275 | concat-map: 0.0.1 276 | dev: true 277 | 278 | /braces@3.0.2: 279 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 280 | engines: {node: '>=8'} 281 | dependencies: 282 | fill-range: 7.0.1 283 | dev: true 284 | 285 | /browserslist@4.22.1: 286 | resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==} 287 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 288 | hasBin: true 289 | dependencies: 290 | caniuse-lite: 1.0.30001551 291 | electron-to-chromium: 1.4.563 292 | node-releases: 2.0.13 293 | update-browserslist-db: 1.0.13(browserslist@4.22.1) 294 | dev: true 295 | 296 | /buffer-from@1.1.2: 297 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 298 | dev: false 299 | 300 | /busboy@1.6.0: 301 | resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} 302 | engines: {node: '>=10.16.0'} 303 | dependencies: 304 | streamsearch: 1.1.0 305 | dev: false 306 | 307 | /camelcase-css@2.0.1: 308 | resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 309 | engines: {node: '>= 6'} 310 | dev: true 311 | 312 | /caniuse-lite@1.0.30001551: 313 | resolution: {integrity: sha512-vtBAez47BoGMMzlbYhfXrMV1kvRF2WP/lqiMuDu1Sb4EE4LKEgjopFDSRtZfdVnslNRpOqV/woE+Xgrwj6VQlg==} 314 | 315 | /chokidar@3.5.3: 316 | resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} 317 | engines: {node: '>= 8.10.0'} 318 | dependencies: 319 | anymatch: 3.1.3 320 | braces: 3.0.2 321 | glob-parent: 5.1.2 322 | is-binary-path: 2.1.0 323 | is-glob: 4.0.3 324 | normalize-path: 3.0.0 325 | readdirp: 3.6.0 326 | optionalDependencies: 327 | fsevents: 2.3.3 328 | dev: true 329 | 330 | /client-only@0.0.1: 331 | resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} 332 | dev: false 333 | 334 | /clsx@2.0.0: 335 | resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} 336 | engines: {node: '>=6'} 337 | dev: false 338 | 339 | /combine-errors@3.0.3: 340 | resolution: {integrity: sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==} 341 | dependencies: 342 | custom-error-instance: 2.1.1 343 | lodash.uniqby: 4.5.0 344 | dev: false 345 | 346 | /commander@4.1.1: 347 | resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 348 | engines: {node: '>= 6'} 349 | dev: true 350 | 351 | /concat-map@0.0.1: 352 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 353 | dev: true 354 | 355 | /cssesc@3.0.0: 356 | resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 357 | engines: {node: '>=4'} 358 | hasBin: true 359 | dev: true 360 | 361 | /csstype@3.1.2: 362 | resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} 363 | dev: true 364 | 365 | /custom-error-instance@2.1.1: 366 | resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==} 367 | dev: false 368 | 369 | /didyoumean@1.2.2: 370 | resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} 371 | dev: true 372 | 373 | /dlv@1.1.3: 374 | resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 375 | dev: true 376 | 377 | /electron-to-chromium@1.4.563: 378 | resolution: {integrity: sha512-dg5gj5qOgfZNkPNeyKBZQAQitIQ/xwfIDmEQJHCbXaD9ebTZxwJXUsDYcBlAvZGZLi+/354l35J1wkmP6CqYaw==} 379 | dev: true 380 | 381 | /escalade@3.1.1: 382 | resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} 383 | engines: {node: '>=6'} 384 | dev: true 385 | 386 | /fast-glob@3.3.1: 387 | resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} 388 | engines: {node: '>=8.6.0'} 389 | dependencies: 390 | '@nodelib/fs.stat': 2.0.5 391 | '@nodelib/fs.walk': 1.2.8 392 | glob-parent: 5.1.2 393 | merge2: 1.4.1 394 | micromatch: 4.0.5 395 | dev: true 396 | 397 | /fastq@1.15.0: 398 | resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} 399 | dependencies: 400 | reusify: 1.0.4 401 | dev: true 402 | 403 | /fill-range@7.0.1: 404 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 405 | engines: {node: '>=8'} 406 | dependencies: 407 | to-regex-range: 5.0.1 408 | dev: true 409 | 410 | /fraction.js@4.3.7: 411 | resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} 412 | dev: true 413 | 414 | /fs.realpath@1.0.0: 415 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 416 | dev: true 417 | 418 | /fsevents@2.3.3: 419 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 420 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 421 | os: [darwin] 422 | requiresBuild: true 423 | dev: true 424 | optional: true 425 | 426 | /function-bind@1.1.2: 427 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 428 | dev: true 429 | 430 | /glob-parent@5.1.2: 431 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 432 | engines: {node: '>= 6'} 433 | dependencies: 434 | is-glob: 4.0.3 435 | dev: true 436 | 437 | /glob-parent@6.0.2: 438 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 439 | engines: {node: '>=10.13.0'} 440 | dependencies: 441 | is-glob: 4.0.3 442 | dev: true 443 | 444 | /glob-to-regexp@0.4.1: 445 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 446 | dev: false 447 | 448 | /glob@7.1.6: 449 | resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} 450 | dependencies: 451 | fs.realpath: 1.0.0 452 | inflight: 1.0.6 453 | inherits: 2.0.4 454 | minimatch: 3.1.2 455 | once: 1.4.0 456 | path-is-absolute: 1.0.1 457 | dev: true 458 | 459 | /graceful-fs@4.2.11: 460 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 461 | dev: false 462 | 463 | /hasown@2.0.0: 464 | resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} 465 | engines: {node: '>= 0.4'} 466 | dependencies: 467 | function-bind: 1.1.2 468 | dev: true 469 | 470 | /inflight@1.0.6: 471 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 472 | dependencies: 473 | once: 1.4.0 474 | wrappy: 1.0.2 475 | dev: true 476 | 477 | /inherits@2.0.4: 478 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 479 | dev: true 480 | 481 | /is-binary-path@2.1.0: 482 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 483 | engines: {node: '>=8'} 484 | dependencies: 485 | binary-extensions: 2.2.0 486 | dev: true 487 | 488 | /is-core-module@2.13.1: 489 | resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} 490 | dependencies: 491 | hasown: 2.0.0 492 | dev: true 493 | 494 | /is-extglob@2.1.1: 495 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 496 | engines: {node: '>=0.10.0'} 497 | dev: true 498 | 499 | /is-glob@4.0.3: 500 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 501 | engines: {node: '>=0.10.0'} 502 | dependencies: 503 | is-extglob: 2.1.1 504 | dev: true 505 | 506 | /is-number@7.0.0: 507 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 508 | engines: {node: '>=0.12.0'} 509 | dev: true 510 | 511 | /is-stream@2.0.1: 512 | resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} 513 | engines: {node: '>=8'} 514 | dev: false 515 | 516 | /jiti@1.20.0: 517 | resolution: {integrity: sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==} 518 | hasBin: true 519 | dev: true 520 | 521 | /js-base64@3.7.5: 522 | resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} 523 | dev: false 524 | 525 | /js-tokens@4.0.0: 526 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 527 | dev: false 528 | 529 | /lilconfig@2.1.0: 530 | resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} 531 | engines: {node: '>=10'} 532 | dev: true 533 | 534 | /lines-and-columns@1.2.4: 535 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 536 | dev: true 537 | 538 | /lodash._baseiteratee@4.7.0: 539 | resolution: {integrity: sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==} 540 | dependencies: 541 | lodash._stringtopath: 4.8.0 542 | dev: false 543 | 544 | /lodash._basetostring@4.12.0: 545 | resolution: {integrity: sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw==} 546 | dev: false 547 | 548 | /lodash._baseuniq@4.6.0: 549 | resolution: {integrity: sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A==} 550 | dependencies: 551 | lodash._createset: 4.0.3 552 | lodash._root: 3.0.1 553 | dev: false 554 | 555 | /lodash._createset@4.0.3: 556 | resolution: {integrity: sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA==} 557 | dev: false 558 | 559 | /lodash._root@3.0.1: 560 | resolution: {integrity: sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==} 561 | dev: false 562 | 563 | /lodash._stringtopath@4.8.0: 564 | resolution: {integrity: sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ==} 565 | dependencies: 566 | lodash._basetostring: 4.12.0 567 | dev: false 568 | 569 | /lodash.throttle@4.1.1: 570 | resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} 571 | dev: false 572 | 573 | /lodash.uniqby@4.5.0: 574 | resolution: {integrity: sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==} 575 | dependencies: 576 | lodash._baseiteratee: 4.7.0 577 | lodash._baseuniq: 4.6.0 578 | dev: false 579 | 580 | /loose-envify@1.4.0: 581 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 582 | hasBin: true 583 | dependencies: 584 | js-tokens: 4.0.0 585 | dev: false 586 | 587 | /merge2@1.4.1: 588 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 589 | engines: {node: '>= 8'} 590 | dev: true 591 | 592 | /micromatch@4.0.5: 593 | resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} 594 | engines: {node: '>=8.6'} 595 | dependencies: 596 | braces: 3.0.2 597 | picomatch: 2.3.1 598 | dev: true 599 | 600 | /minimatch@3.1.2: 601 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 602 | dependencies: 603 | brace-expansion: 1.1.11 604 | dev: true 605 | 606 | /mz@2.7.0: 607 | resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 608 | dependencies: 609 | any-promise: 1.3.0 610 | object-assign: 4.1.1 611 | thenify-all: 1.6.0 612 | dev: true 613 | 614 | /nanoid@3.3.6: 615 | resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} 616 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 617 | hasBin: true 618 | 619 | /next@13.5.6(react-dom@18.2.0)(react@18.2.0): 620 | resolution: {integrity: sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==} 621 | engines: {node: '>=16.14.0'} 622 | hasBin: true 623 | peerDependencies: 624 | '@opentelemetry/api': ^1.1.0 625 | react: ^18.2.0 626 | react-dom: ^18.2.0 627 | sass: ^1.3.0 628 | peerDependenciesMeta: 629 | '@opentelemetry/api': 630 | optional: true 631 | sass: 632 | optional: true 633 | dependencies: 634 | '@next/env': 13.5.6 635 | '@swc/helpers': 0.5.2 636 | busboy: 1.6.0 637 | caniuse-lite: 1.0.30001551 638 | postcss: 8.4.31 639 | react: 18.2.0 640 | react-dom: 18.2.0(react@18.2.0) 641 | styled-jsx: 5.1.1(react@18.2.0) 642 | watchpack: 2.4.0 643 | optionalDependencies: 644 | '@next/swc-darwin-arm64': 13.5.6 645 | '@next/swc-darwin-x64': 13.5.6 646 | '@next/swc-linux-arm64-gnu': 13.5.6 647 | '@next/swc-linux-arm64-musl': 13.5.6 648 | '@next/swc-linux-x64-gnu': 13.5.6 649 | '@next/swc-linux-x64-musl': 13.5.6 650 | '@next/swc-win32-arm64-msvc': 13.5.6 651 | '@next/swc-win32-ia32-msvc': 13.5.6 652 | '@next/swc-win32-x64-msvc': 13.5.6 653 | transitivePeerDependencies: 654 | - '@babel/core' 655 | - babel-plugin-macros 656 | dev: false 657 | 658 | /node-releases@2.0.13: 659 | resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} 660 | dev: true 661 | 662 | /normalize-path@3.0.0: 663 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 664 | engines: {node: '>=0.10.0'} 665 | dev: true 666 | 667 | /normalize-range@0.1.2: 668 | resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} 669 | engines: {node: '>=0.10.0'} 670 | dev: true 671 | 672 | /object-assign@4.1.1: 673 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 674 | engines: {node: '>=0.10.0'} 675 | dev: true 676 | 677 | /object-hash@3.0.0: 678 | resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} 679 | engines: {node: '>= 6'} 680 | dev: true 681 | 682 | /once@1.4.0: 683 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 684 | dependencies: 685 | wrappy: 1.0.2 686 | dev: true 687 | 688 | /path-is-absolute@1.0.1: 689 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 690 | engines: {node: '>=0.10.0'} 691 | dev: true 692 | 693 | /path-parse@1.0.7: 694 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 695 | dev: true 696 | 697 | /picocolors@1.0.0: 698 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 699 | 700 | /picomatch@2.3.1: 701 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 702 | engines: {node: '>=8.6'} 703 | dev: true 704 | 705 | /pify@2.3.0: 706 | resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} 707 | engines: {node: '>=0.10.0'} 708 | dev: true 709 | 710 | /pirates@4.0.6: 711 | resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 712 | engines: {node: '>= 6'} 713 | dev: true 714 | 715 | /postcss-import@15.1.0(postcss@8.4.31): 716 | resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} 717 | engines: {node: '>=14.0.0'} 718 | peerDependencies: 719 | postcss: ^8.0.0 720 | dependencies: 721 | postcss: 8.4.31 722 | postcss-value-parser: 4.2.0 723 | read-cache: 1.0.0 724 | resolve: 1.22.8 725 | dev: true 726 | 727 | /postcss-js@4.0.1(postcss@8.4.31): 728 | resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} 729 | engines: {node: ^12 || ^14 || >= 16} 730 | peerDependencies: 731 | postcss: ^8.4.21 732 | dependencies: 733 | camelcase-css: 2.0.1 734 | postcss: 8.4.31 735 | dev: true 736 | 737 | /postcss-load-config@4.0.1(postcss@8.4.31): 738 | resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} 739 | engines: {node: '>= 14'} 740 | peerDependencies: 741 | postcss: '>=8.0.9' 742 | ts-node: '>=9.0.0' 743 | peerDependenciesMeta: 744 | postcss: 745 | optional: true 746 | ts-node: 747 | optional: true 748 | dependencies: 749 | lilconfig: 2.1.0 750 | postcss: 8.4.31 751 | yaml: 2.3.3 752 | dev: true 753 | 754 | /postcss-nested@6.0.1(postcss@8.4.31): 755 | resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} 756 | engines: {node: '>=12.0'} 757 | peerDependencies: 758 | postcss: ^8.2.14 759 | dependencies: 760 | postcss: 8.4.31 761 | postcss-selector-parser: 6.0.13 762 | dev: true 763 | 764 | /postcss-selector-parser@6.0.13: 765 | resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} 766 | engines: {node: '>=4'} 767 | dependencies: 768 | cssesc: 3.0.0 769 | util-deprecate: 1.0.2 770 | dev: true 771 | 772 | /postcss-value-parser@4.2.0: 773 | resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 774 | dev: true 775 | 776 | /postcss@8.4.31: 777 | resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} 778 | engines: {node: ^10 || ^12 || >=14} 779 | dependencies: 780 | nanoid: 3.3.6 781 | picocolors: 1.0.0 782 | source-map-js: 1.0.2 783 | 784 | /prettier@3.0.3: 785 | resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==} 786 | engines: {node: '>=14'} 787 | hasBin: true 788 | dev: true 789 | 790 | /proper-lockfile@4.1.2: 791 | resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} 792 | dependencies: 793 | graceful-fs: 4.2.11 794 | retry: 0.12.0 795 | signal-exit: 3.0.7 796 | dev: false 797 | 798 | /querystringify@2.2.0: 799 | resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} 800 | dev: false 801 | 802 | /queue-microtask@1.2.3: 803 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 804 | dev: true 805 | 806 | /react-dom@18.2.0(react@18.2.0): 807 | resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} 808 | peerDependencies: 809 | react: ^18.2.0 810 | dependencies: 811 | loose-envify: 1.4.0 812 | react: 18.2.0 813 | scheduler: 0.23.0 814 | dev: false 815 | 816 | /react@18.2.0: 817 | resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} 818 | engines: {node: '>=0.10.0'} 819 | dependencies: 820 | loose-envify: 1.4.0 821 | dev: false 822 | 823 | /read-cache@1.0.0: 824 | resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} 825 | dependencies: 826 | pify: 2.3.0 827 | dev: true 828 | 829 | /readdirp@3.6.0: 830 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 831 | engines: {node: '>=8.10.0'} 832 | dependencies: 833 | picomatch: 2.3.1 834 | dev: true 835 | 836 | /requires-port@1.0.0: 837 | resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} 838 | dev: false 839 | 840 | /resolve@1.22.8: 841 | resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} 842 | hasBin: true 843 | dependencies: 844 | is-core-module: 2.13.1 845 | path-parse: 1.0.7 846 | supports-preserve-symlinks-flag: 1.0.0 847 | dev: true 848 | 849 | /retry@0.12.0: 850 | resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} 851 | engines: {node: '>= 4'} 852 | dev: false 853 | 854 | /reusify@1.0.4: 855 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 856 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 857 | dev: true 858 | 859 | /run-parallel@1.2.0: 860 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 861 | dependencies: 862 | queue-microtask: 1.2.3 863 | dev: true 864 | 865 | /scheduler@0.23.0: 866 | resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} 867 | dependencies: 868 | loose-envify: 1.4.0 869 | dev: false 870 | 871 | /signal-exit@3.0.7: 872 | resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} 873 | dev: false 874 | 875 | /source-map-js@1.0.2: 876 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 877 | engines: {node: '>=0.10.0'} 878 | 879 | /streamsearch@1.1.0: 880 | resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} 881 | engines: {node: '>=10.0.0'} 882 | dev: false 883 | 884 | /styled-jsx@5.1.1(react@18.2.0): 885 | resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} 886 | engines: {node: '>= 12.0.0'} 887 | peerDependencies: 888 | '@babel/core': '*' 889 | babel-plugin-macros: '*' 890 | react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' 891 | peerDependenciesMeta: 892 | '@babel/core': 893 | optional: true 894 | babel-plugin-macros: 895 | optional: true 896 | dependencies: 897 | client-only: 0.0.1 898 | react: 18.2.0 899 | dev: false 900 | 901 | /sucrase@3.34.0: 902 | resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} 903 | engines: {node: '>=8'} 904 | hasBin: true 905 | dependencies: 906 | '@jridgewell/gen-mapping': 0.3.3 907 | commander: 4.1.1 908 | glob: 7.1.6 909 | lines-and-columns: 1.2.4 910 | mz: 2.7.0 911 | pirates: 4.0.6 912 | ts-interface-checker: 0.1.13 913 | dev: true 914 | 915 | /supports-preserve-symlinks-flag@1.0.0: 916 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 917 | engines: {node: '>= 0.4'} 918 | dev: true 919 | 920 | /tailwindcss@3.3.3: 921 | resolution: {integrity: sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==} 922 | engines: {node: '>=14.0.0'} 923 | hasBin: true 924 | dependencies: 925 | '@alloc/quick-lru': 5.2.0 926 | arg: 5.0.2 927 | chokidar: 3.5.3 928 | didyoumean: 1.2.2 929 | dlv: 1.1.3 930 | fast-glob: 3.3.1 931 | glob-parent: 6.0.2 932 | is-glob: 4.0.3 933 | jiti: 1.20.0 934 | lilconfig: 2.1.0 935 | micromatch: 4.0.5 936 | normalize-path: 3.0.0 937 | object-hash: 3.0.0 938 | picocolors: 1.0.0 939 | postcss: 8.4.31 940 | postcss-import: 15.1.0(postcss@8.4.31) 941 | postcss-js: 4.0.1(postcss@8.4.31) 942 | postcss-load-config: 4.0.1(postcss@8.4.31) 943 | postcss-nested: 6.0.1(postcss@8.4.31) 944 | postcss-selector-parser: 6.0.13 945 | resolve: 1.22.8 946 | sucrase: 3.34.0 947 | transitivePeerDependencies: 948 | - ts-node 949 | dev: true 950 | 951 | /thenify-all@1.6.0: 952 | resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 953 | engines: {node: '>=0.8'} 954 | dependencies: 955 | thenify: 3.3.1 956 | dev: true 957 | 958 | /thenify@3.3.1: 959 | resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 960 | dependencies: 961 | any-promise: 1.3.0 962 | dev: true 963 | 964 | /to-regex-range@5.0.1: 965 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 966 | engines: {node: '>=8.0'} 967 | dependencies: 968 | is-number: 7.0.0 969 | dev: true 970 | 971 | /ts-interface-checker@0.1.13: 972 | resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 973 | dev: true 974 | 975 | /tslib@2.6.2: 976 | resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} 977 | dev: false 978 | 979 | /tus-js-client@3.1.1: 980 | resolution: {integrity: sha512-SZzWP62jEFLmROSRZx+uoGLKqsYWMGK/m+PiNehPVWbCm7/S9zRIMaDxiaOcKdMnFno4luaqP5E+Y1iXXPjP0A==} 981 | dependencies: 982 | buffer-from: 1.1.2 983 | combine-errors: 3.0.3 984 | is-stream: 2.0.1 985 | js-base64: 3.7.5 986 | lodash.throttle: 4.1.1 987 | proper-lockfile: 4.1.2 988 | url-parse: 1.5.10 989 | dev: false 990 | 991 | /typescript@5.2.2: 992 | resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} 993 | engines: {node: '>=14.17'} 994 | hasBin: true 995 | dev: true 996 | 997 | /undici-types@5.25.3: 998 | resolution: {integrity: sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==} 999 | dev: true 1000 | 1001 | /update-browserslist-db@1.0.13(browserslist@4.22.1): 1002 | resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} 1003 | hasBin: true 1004 | peerDependencies: 1005 | browserslist: '>= 4.21.0' 1006 | dependencies: 1007 | browserslist: 4.22.1 1008 | escalade: 3.1.1 1009 | picocolors: 1.0.0 1010 | dev: true 1011 | 1012 | /url-parse@1.5.10: 1013 | resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} 1014 | dependencies: 1015 | querystringify: 2.2.0 1016 | requires-port: 1.0.0 1017 | dev: false 1018 | 1019 | /use-tus@0.7.3(react@18.2.0)(tus-js-client@3.1.1): 1020 | resolution: {integrity: sha512-fNf1JHc7dmA7j6aKA6a4g5sTXsJmROuzDse/Yrx8riuboUZ8VJKH0nvdST9zziw5oj++BM3nuTOv83THuM1AeA==} 1021 | peerDependencies: 1022 | react: '>=16.8' 1023 | tus-js-client: ^2.2.0 1024 | dependencies: 1025 | react: 18.2.0 1026 | tus-js-client: 3.1.1 1027 | dev: false 1028 | 1029 | /util-deprecate@1.0.2: 1030 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1031 | dev: true 1032 | 1033 | /watchpack@2.4.0: 1034 | resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} 1035 | engines: {node: '>=10.13.0'} 1036 | dependencies: 1037 | glob-to-regexp: 0.4.1 1038 | graceful-fs: 4.2.11 1039 | dev: false 1040 | 1041 | /wrappy@1.0.2: 1042 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 1043 | dev: true 1044 | 1045 | /yaml@2.3.3: 1046 | resolution: {integrity: sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==} 1047 | engines: {node: '>= 14'} 1048 | dev: true 1049 | -------------------------------------------------------------------------------- /examples/next/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/next/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next/src/app/components/BasicButton/BasicButton.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { ComponentProps, forwardRef } from "react"; 3 | 4 | type BasicButtonProps = { 5 | onClick: ComponentProps<"button">["onClick"]; 6 | title: string; 7 | disabled?: ComponentProps<"button">["disabled"]; 8 | styleColor?: "primary" | "basic" | "error"; 9 | }; 10 | 11 | export const BasicButton = forwardRef( 12 | ({ onClick, title, disabled, styleColor = "primary" }, ref) => ( 13 | 32 | ) 33 | ); 34 | -------------------------------------------------------------------------------- /examples/next/src/app/components/BasicButton/index.ts: -------------------------------------------------------------------------------- 1 | export { BasicButton } from "./BasicButton"; 2 | -------------------------------------------------------------------------------- /examples/next/src/app/components/Loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | 3 | // Inspired From: https://github.com/SamHerbert/SVG-Loaders (MIT License) 4 | export const Loading: FC = () => ( 5 | 11 | 12 | 13 | 14 | 15 | 23 | 24 | 25 | 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /examples/next/src/app/components/Loading/index.ts: -------------------------------------------------------------------------------- 1 | export { Loading } from "./Loading"; 2 | -------------------------------------------------------------------------------- /examples/next/src/app/components/ProgressBar/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | type ProgressBarProps = { 4 | title?: string; 5 | value?: number; 6 | }; 7 | 8 | export const ProgressBar: FC = ({ value = 0, title }) => ( 9 |
10 | {title && ( 11 |
12 | {title} 13 |
14 | )} 15 |
16 |
20 |
21 |
22 | ); 23 | -------------------------------------------------------------------------------- /examples/next/src/app/components/ProgressBar/index.ts: -------------------------------------------------------------------------------- 1 | export { ProgressBar } from "./ProgressBar"; 2 | -------------------------------------------------------------------------------- /examples/next/src/app/components/UploadIcon/UploadIcon.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | export const UploadIcon: FC = () => ( 4 |
5 | 12 | 13 | 14 | 15 |
16 | ); 17 | -------------------------------------------------------------------------------- /examples/next/src/app/components/UploadIcon/index.ts: -------------------------------------------------------------------------------- 1 | export { UploadIcon } from "./UploadIcon"; 2 | -------------------------------------------------------------------------------- /examples/next/src/app/components/Uploader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRef, useState, useMemo, useCallback, ChangeEvent } from "react"; 4 | import { useTus } from "use-tus"; 5 | import { BasicButton } from "./BasicButton"; 6 | import { Loading } from "./Loading"; 7 | import { ProgressBar } from "./ProgressBar"; 8 | import { UploadIcon } from "./UploadIcon"; 9 | 10 | export const TUS_DEMO_ENDPOINT = "https://tusd.tusdemo.net/files/"; 11 | 12 | export const Uploader = () => { 13 | const inputRef = useRef(null); 14 | const { upload, setUpload, isSuccess, isAborted, isUploading } = useTus({ 15 | autoStart: true, 16 | }); 17 | const [progress, setProgress] = useState(0); 18 | const uploadedUrl = useMemo( 19 | () => isSuccess && upload?.url, 20 | [upload, isSuccess] 21 | ); 22 | 23 | const handleOnSelectFile = () => { 24 | if (!inputRef.current) { 25 | return; 26 | } 27 | 28 | inputRef.current.click(); 29 | }; 30 | 31 | const handleOnSetUpload = useCallback( 32 | (event: ChangeEvent) => { 33 | const file = event.target?.files?.item(0); 34 | 35 | if (!file) { 36 | return; 37 | } 38 | 39 | setUpload(file, { 40 | endpoint: TUS_DEMO_ENDPOINT, 41 | chunkSize: file.size / 10, 42 | metadata: { 43 | filename: file.name, 44 | filetype: file.type, 45 | }, 46 | onProgress: (bytesSent, bytesTotal) => { 47 | setProgress(Number(((bytesSent / bytesTotal) * 100).toFixed(2))); 48 | }, 49 | onSuccess: (upload) => { 50 | // eslint-disable-next-line no-console 51 | console.info("upload success", upload.url); 52 | }, 53 | }); 54 | }, 55 | [setUpload] 56 | ); 57 | 58 | const handleOnStart = useCallback(() => { 59 | if (!upload) { 60 | return; 61 | } 62 | 63 | upload.start(); 64 | }, [upload]); 65 | 66 | const handleOnAbort = useCallback(async () => { 67 | if (!upload) { 68 | return; 69 | } 70 | 71 | await upload.abort(); 72 | }, [upload]); 73 | 74 | return ( 75 |
76 |
77 | 78 |
79 |
80 |

81 | In this demo, you can upload to the demo-only server provided by tus 82 | official. 83 |

84 |

Also, please be careful about the images you upload.

85 |
86 |
87 | 88 |
89 | 90 |
91 | 96 | 102 | 108 |
109 | {isUploading && ( 110 |
111 | 112 |
113 | )} 114 | {uploadedUrl && ( 115 |
116 | upload 117 |
118 | )} 119 |
120 | ); 121 | }; 122 | -------------------------------------------------------------------------------- /examples/next/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kqito/use-tus/4e74fd0348dd05ed809f425068cb66f23649d3bc/examples/next/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/next/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /examples/next/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | // eslint-disable-next-line import/extensions 3 | import { Inter } from "next/font/google"; 4 | import "./globals.css"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "Create Next App", 10 | description: "Generated by create next app", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /examples/next/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { Uploader } from "./components/Uploader"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 |
8 |

9 | Get started by editing  10 | src/app/page.tsx 11 |

12 | 30 |
31 | 32 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /examples/next/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /examples/next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | collectCoverage: true, 4 | testEnvironment: "jsdom", 5 | coverageDirectory: "./coverage/", 6 | transform: { 7 | "^.+\\.(t|j)sx?$": [ 8 | "ts-jest", 9 | { 10 | tsconfig: "./tsconfig.json", 11 | }, 12 | ], 13 | }, 14 | roots: ["/src"], 15 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 16 | testMatch: ["**/__tests__/**/*.test.ts?(x)", "**/?(*.)+(spec|test).ts?(x)"], 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-tus", 3 | "version": "0.8.2", 4 | "description": "React hooks for resumable file uploads using tus-js-client", 5 | "main": "dist/index.cjs.js", 6 | "module": "dist/index.esm.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "build": "npm-run-all -s type clean build:rollup", 10 | "build:rollup": "cross-env NODE_ENV=production rollup -c", 11 | "test": "cross-env NODE_ENV=TEST jest", 12 | "type": "tsc --noEmit", 13 | "format": "npm-run-all -s format:*", 14 | "format:fix": "prettier --write './**/*.{ts,tsx,js,jsx,json}'", 15 | "format:lint": "eslint ./ --ext .ts,.tsx", 16 | "clean": "rimraf ./dist", 17 | "prepublishOnly": "npm run build", 18 | "prepare": "husky install", 19 | "storybook": "storybook dev -p 6006", 20 | "storybook:build": "storybook build", 21 | "storybook:static": "gh-pages -d storybook-static", 22 | "storybook:deploy": "npm-run-all -s storybook:build storybook:static" 23 | }, 24 | "keywords": [ 25 | "react", 26 | "react-hooks", 27 | "hooks", 28 | "tus", 29 | "tus-js-client" 30 | ], 31 | "files": [ 32 | "dist", 33 | "LICENSE", 34 | "README.md" 35 | ], 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/kqito/use-tus.git" 39 | }, 40 | "author": "Nakagawa kaito", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/kqito/use-tus/issues" 44 | }, 45 | "homepage": "https://github.com/kqito/use-tus#readme", 46 | "devDependencies": { 47 | "@rollup/plugin-commonjs": "^18.0.0", 48 | "@rollup/plugin-node-resolve": "^11.2.1", 49 | "@rollup/plugin-typescript": "^8.3.0", 50 | "@storybook/addon-actions": "^7.5.1", 51 | "@storybook/addon-essentials": "^7.5.1", 52 | "@storybook/addon-links": "^7.5.1", 53 | "@storybook/react": "^7.5.1", 54 | "@storybook/react-vite": "^7.5.1", 55 | "@testing-library/jest-dom": "^6.1.4", 56 | "@testing-library/react": "^13.3.0", 57 | "@types/jest": "^29.5.6", 58 | "@types/react": "^17.0.3", 59 | "@types/react-dom": "^17.0.3", 60 | "@typescript-eslint/eslint-plugin": "^6.8.0", 61 | "@typescript-eslint/parser": "^6.8.0", 62 | "autoprefixer": "^9", 63 | "clsx": "^1.1.1", 64 | "cross-env": "^7.0.3", 65 | "esbuild": "^0.19.5", 66 | "eslint": "^8.52.0", 67 | "eslint-config-airbnb": "^19.0.4", 68 | "eslint-config-prettier": "^9.0.0", 69 | "eslint-plugin-import": "^2.28.1", 70 | "eslint-plugin-jsx-a11y": "^6.7.1", 71 | "eslint-plugin-prettier": "^5.0.1", 72 | "eslint-plugin-react": "^7.33.2", 73 | "eslint-plugin-react-hooks": "^4.6.0", 74 | "eslint-plugin-storybook": "^0.6.15", 75 | "gh-pages": "^3.1.0", 76 | "husky": "^6.0.0", 77 | "jest": "^29.7.0", 78 | "jest-dom": "^4.0.0", 79 | "jest-environment-jsdom": "^29.7.0", 80 | "lint-staged": "^10.5.4", 81 | "npm-run-all": "^4.1.5", 82 | "postcss": "^7", 83 | "prettier": "^2.2.1", 84 | "react": "^18.1.0", 85 | "react-dom": "^18.1.0", 86 | "rimraf": "^3.0.2", 87 | "rollup": "^2.44.0", 88 | "rollup-plugin-esbuild": "^6.1.0", 89 | "storybook": "^7.5.1", 90 | "tailwindcss": "npm:@tailwindcss/postcss7-compat", 91 | "ts-jest": "^29.1.1", 92 | "tus-js-client": "^4.0.0", 93 | "typescript": "^5.2.2", 94 | "vite": "^4.5.0" 95 | }, 96 | "peerDependencies": { 97 | "react": ">=16.8", 98 | "tus-js-client": ">=2.2.0" 99 | }, 100 | "packageManager": "pnpm@8.8.0" 101 | } 102 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // eslint-disable-next-line global-require 3 | plugins: [require("tailwindcss")("./src/__stories__/tailwind.config.js")], 4 | }; 5 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import esbuild from "rollup-plugin-esbuild"; 4 | import path from "path"; 5 | import pkg from "./package.json"; 6 | 7 | const extensions = [".js", ".ts", ".tsx"]; 8 | 9 | const distDir = "dist"; 10 | const baseConfig = { 11 | input: "src/index.ts", 12 | external: ["react", "react-dom", ...Object.keys(pkg.peerDependencies)], 13 | }; 14 | 15 | const dtsConfig = { 16 | ...baseConfig, 17 | output: { 18 | dir: distDir, 19 | }, 20 | plugins: [ 21 | typescript({ 22 | declaration: true, 23 | emitDeclarationOnly: true, 24 | outDir: distDir, 25 | }), 26 | ], 27 | }; 28 | 29 | const cjsConfig = { 30 | ...baseConfig, 31 | output: { file: pkg.main, format: "cjs" }, 32 | plugins: [ 33 | resolve({ 34 | extensions, 35 | }), 36 | esbuild({ 37 | minify: true, 38 | tsconfig: path.resolve("./tsconfig.json"), 39 | }), 40 | ], 41 | }; 42 | 43 | const mjsConfig = { 44 | ...baseConfig, 45 | output: { file: pkg.module, format: "esm" }, 46 | plugins: [ 47 | resolve({ 48 | extensions, 49 | }), 50 | esbuild({ 51 | minify: true, 52 | target: "node12", 53 | tsconfig: path.resolve("./tsconfig.json"), 54 | }), 55 | ], 56 | }; 57 | 58 | export default [dtsConfig, cjsConfig, mjsConfig]; 59 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | -------------------------------------------------------------------------------- /src/TusClientProvider/TusClientProvider.tsx: -------------------------------------------------------------------------------- 1 | import { isSupported } from "tus-js-client"; 2 | import { useReducer, createElement, FC, useEffect } from "react"; 3 | import { DefaultOptions } from "./types"; 4 | import { 5 | TusClientDispatchContext, 6 | TusClientStateContext, 7 | } from "./store/contexts"; 8 | import { updateDefaultOptions } from "./store/tucClientActions"; 9 | import { 10 | tusClientReducer, 11 | tusClientInitialState, 12 | } from "./store/tusClientReducer"; 13 | import { ERROR_MESSAGES } from "./constants"; 14 | 15 | export type TusClientProviderProps = Readonly<{ 16 | children?: React.ReactNode; 17 | defaultOptions?: DefaultOptions; 18 | }>; 19 | 20 | export const TusClientProvider: FC = ({ 21 | defaultOptions, 22 | children, 23 | }) => { 24 | const [tusClientState, tusClientDispatch] = useReducer(tusClientReducer, { 25 | ...tusClientInitialState, 26 | defaultOptions, 27 | }); 28 | 29 | // Output error if tus has not supported 30 | useEffect(() => { 31 | if (isSupported || process.env.NODE_ENV === "production") { 32 | return; 33 | } 34 | 35 | // eslint-disable-next-line no-console 36 | console.error(ERROR_MESSAGES.tusIsNotSupported); 37 | }, []); 38 | 39 | // Set defaultOptions to the context 40 | useEffect(() => { 41 | if (tusClientState.defaultOptions === defaultOptions) { 42 | return; 43 | } 44 | 45 | tusClientDispatch(updateDefaultOptions(defaultOptions)); 46 | }, [defaultOptions, tusClientState.defaultOptions]); 47 | 48 | const tusClientDispatchContextProviderElement = createElement( 49 | TusClientDispatchContext.Provider, 50 | { value: tusClientDispatch }, 51 | children 52 | ); 53 | 54 | return createElement( 55 | TusClientStateContext.Provider, 56 | { 57 | value: tusClientState, 58 | }, 59 | tusClientDispatchContextProviderElement 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/TusClientProvider/constants.ts: -------------------------------------------------------------------------------- 1 | export const ERROR_MESSAGES = { 2 | tusClientHasNotFounded: "No TusClient set, use TusClientProvider to set one", 3 | tusIsNotSupported: 4 | "This browser does not support uploads. Please use a modern browser instead.", 5 | } as const; 6 | -------------------------------------------------------------------------------- /src/TusClientProvider/index.ts: -------------------------------------------------------------------------------- 1 | export { TusClientProvider } from "./TusClientProvider"; 2 | export type { TusClientProviderProps } from "./TusClientProvider"; 3 | export type { DefaultOptions } from "./types"; 4 | -------------------------------------------------------------------------------- /src/TusClientProvider/store/contexts.ts: -------------------------------------------------------------------------------- 1 | import type { Dispatch } from "react"; 2 | import { createContext, useContext, useMemo } from "react"; 3 | import { ERROR_MESSAGES } from "../constants"; 4 | import { TusClientActions } from "./tucClientActions"; 5 | import { TusClientState } from "./tusClientReducer"; 6 | 7 | export const TusClientStateContext = createContext( 8 | undefined 9 | ); 10 | export const TusClientDispatchContext = createContext< 11 | Dispatch | undefined 12 | >(undefined); 13 | 14 | export const useTusClientState = () => { 15 | const tusClientState = useContext(TusClientStateContext); 16 | 17 | if (!tusClientState && process.env.NODE_ENV !== "production") { 18 | throw new Error(ERROR_MESSAGES.tusClientHasNotFounded); 19 | } 20 | 21 | return useMemo(() => tusClientState as TusClientState, [tusClientState]); 22 | }; 23 | 24 | export const useTusClientDispatch = () => { 25 | const tusClientDispatch = useContext(TusClientDispatchContext); 26 | 27 | if (!tusClientDispatch && process.env.NODE_ENV !== "production") { 28 | throw new Error(ERROR_MESSAGES.tusClientHasNotFounded); 29 | } 30 | 31 | return useMemo( 32 | () => tusClientDispatch as Dispatch, 33 | [tusClientDispatch] 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/TusClientProvider/store/tucClientActions.ts: -------------------------------------------------------------------------------- 1 | import { DefaultOptions } from "../types"; 2 | import { TusTruthlyContext } from "../../types"; 3 | 4 | export type TusClientActions = ReturnType< 5 | | typeof insertUploadInstance 6 | | typeof removeUploadInstance 7 | | typeof updateUploadContext 8 | | typeof resetClient 9 | | typeof updateDefaultOptions 10 | >; 11 | 12 | export const insertUploadInstance = ( 13 | cacheKey: string, 14 | state: TusTruthlyContext 15 | ) => 16 | ({ 17 | type: "INSERT_UPLOAD_INSTANCE", 18 | payload: { 19 | cacheKey, 20 | uploadState: state, 21 | }, 22 | } as const); 23 | 24 | export const updateUploadContext = ( 25 | cacheKey: string, 26 | context: Partial 27 | ) => 28 | ({ 29 | type: "UPDATE_UPLOAD_CONTEXT", 30 | payload: { 31 | cacheKey, 32 | context, 33 | }, 34 | } as const); 35 | 36 | export const removeUploadInstance = (cacheKey: string) => 37 | ({ 38 | type: "REMOVE_UPLOAD_INSTANCE", 39 | payload: { 40 | cacheKey, 41 | }, 42 | } as const); 43 | 44 | export const resetClient = () => 45 | ({ 46 | type: "RESET_CLIENT", 47 | } as const); 48 | 49 | export const updateDefaultOptions = ( 50 | defaultOptions: DefaultOptions | undefined 51 | ) => 52 | ({ 53 | type: "UPDATE_DEFAULT_OPTIONS", 54 | payload: { 55 | defaultOptions, 56 | }, 57 | } as const); 58 | -------------------------------------------------------------------------------- /src/TusClientProvider/store/tusClientReducer.ts: -------------------------------------------------------------------------------- 1 | import type { Reducer } from "react"; 2 | import { DefaultOptions } from "../types"; 3 | import { TusClientActions } from "./tucClientActions"; 4 | import { TusTruthlyContext } from "../../types"; 5 | 6 | export type TusClientState = { 7 | uploads: { 8 | [cacheKey: string]: TusTruthlyContext | undefined; 9 | }; 10 | defaultOptions: DefaultOptions | undefined; 11 | }; 12 | 13 | export const tusClientReducer: Reducer = ( 14 | state, 15 | actions 16 | ) => { 17 | switch (actions.type) { 18 | case "INSERT_UPLOAD_INSTANCE": { 19 | const { cacheKey, uploadState } = actions.payload; 20 | 21 | return { 22 | ...state, 23 | uploads: { 24 | ...state.uploads, 25 | [cacheKey]: uploadState, 26 | }, 27 | }; 28 | } 29 | 30 | case "UPDATE_UPLOAD_CONTEXT": { 31 | const { cacheKey, context } = actions.payload; 32 | 33 | const target = state.uploads[cacheKey]; 34 | 35 | if (!target) { 36 | return state; 37 | } 38 | 39 | return { 40 | ...state, 41 | uploads: { ...state.uploads, [cacheKey]: { ...target, ...context } }, 42 | }; 43 | } 44 | 45 | case "REMOVE_UPLOAD_INSTANCE": { 46 | const { cacheKey } = actions.payload; 47 | 48 | const newUploads = state.uploads; 49 | delete newUploads[cacheKey]; 50 | 51 | return { 52 | ...state, 53 | uploads: newUploads, 54 | }; 55 | } 56 | 57 | case "UPDATE_DEFAULT_OPTIONS": { 58 | const { defaultOptions } = actions.payload; 59 | 60 | return { 61 | ...state, 62 | defaultOptions, 63 | }; 64 | } 65 | 66 | case "RESET_CLIENT": { 67 | return { 68 | ...state, 69 | uploads: {}, 70 | }; 71 | } 72 | 73 | default: { 74 | actions satisfies never; 75 | return state; 76 | } 77 | } 78 | }; 79 | 80 | export const tusClientInitialState: TusClientState = { 81 | uploads: {}, 82 | defaultOptions: undefined, 83 | }; 84 | -------------------------------------------------------------------------------- /src/TusClientProvider/types.ts: -------------------------------------------------------------------------------- 1 | import { Upload } from "tus-js-client"; 2 | import { TusHooksUploadOptions } from "../types"; 3 | 4 | export type DefaultOptions = (file: Upload["file"]) => TusHooksUploadOptions; 5 | -------------------------------------------------------------------------------- /src/__stories__/Basic.stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { Meta } from "@storybook/react"; 3 | import { 4 | ChangeEvent, 5 | useCallback, 6 | useEffect, 7 | useMemo, 8 | useRef, 9 | useState, 10 | } from "react"; 11 | import { ProgressBar } from "./components/ProgressBar"; 12 | 13 | import { useTus } from "../index"; 14 | import { BasicButton } from "./components/BasicButton"; 15 | import { TUS_DEMO_ENDPOINT } from "./constants"; 16 | import { UploadIcon } from "./components/UploadIcon"; 17 | import { LoadingCircle } from "./components/LoadingCircle"; 18 | 19 | export default { 20 | title: "useTus", 21 | } satisfies Meta; 22 | 23 | export const Basic = () => ; 24 | 25 | const Uploader = () => { 26 | const inputRef = useRef(null); 27 | const { upload, setUpload, isSuccess, isAborted, isUploading } = useTus({ 28 | autoStart: true, 29 | }); 30 | 31 | const [progress, setProgress] = useState(0); 32 | const uploadedUrl = useMemo( 33 | () => isSuccess && upload?.url, 34 | [upload, isSuccess] 35 | ); 36 | 37 | const handleOnSelectFile = () => { 38 | if (!inputRef.current) { 39 | return; 40 | } 41 | 42 | inputRef.current.click(); 43 | }; 44 | 45 | const handleOnSetUpload = useCallback( 46 | (event: ChangeEvent) => { 47 | const file = event.target?.files?.item(0); 48 | 49 | if (!file) { 50 | return; 51 | } 52 | 53 | setUpload(file, { 54 | endpoint: TUS_DEMO_ENDPOINT, 55 | chunkSize: file.size / 10, 56 | metadata: { 57 | filename: file.name, 58 | filetype: file.type, 59 | }, 60 | onProgress: (bytesSent, bytesTotal, u) => { 61 | console.log("onProgress", u); 62 | setProgress(Number(((bytesSent / bytesTotal) * 100).toFixed(2))); 63 | }, 64 | onSuccess: (u) => { 65 | console.log("onSuccess", u); 66 | }, 67 | onError: (_, u) => { 68 | console.log("onError", u); 69 | }, 70 | onShouldRetry: (error, u) => { 71 | console.log("onShouldRetry", error, u); 72 | return true; 73 | }, 74 | onChunkComplete: (chunkSize, bytesAccepted, bytesTotal, u) => { 75 | console.log( 76 | "onChunkComplete", 77 | chunkSize, 78 | bytesAccepted, 79 | bytesTotal, 80 | u 81 | ); 82 | }, 83 | onBeforeRequest: (request, u) => { 84 | console.log("onBeforeRequest", request, u); 85 | }, 86 | }); 87 | }, 88 | [setUpload] 89 | ); 90 | 91 | useEffect(() => { 92 | console.log("useEffect", upload?.url); 93 | }, [upload?.url]); 94 | 95 | const handleOnStart = useCallback(() => { 96 | if (!upload) { 97 | return; 98 | } 99 | 100 | upload.start(); 101 | }, [upload]); 102 | 103 | const handleOnAbort = useCallback(async () => { 104 | if (!upload) { 105 | return; 106 | } 107 | 108 | await upload.abort(); 109 | }, [upload]); 110 | 111 | return ( 112 |
113 |
114 |
115 | 116 |
117 |
118 |

119 | In this demo, you can upload to the demo-only server provided by tus 120 | official. 121 |

122 |

Also, please be careful about the images you upload.

123 |
124 |
125 | 126 |
127 | 128 |
129 | 134 | 140 | 146 |
147 | {isUploading && ( 148 |
149 | 150 |
151 | )} 152 | {uploadedUrl && ( 153 |
154 | upload 155 |
156 | )} 157 |
158 |
159 | ); 160 | }; 161 | -------------------------------------------------------------------------------- /src/__stories__/CacheKey.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from "@storybook/react"; 2 | import { ChangeEvent, useCallback, useMemo, useRef, useState } from "react"; 3 | import { ProgressBar } from "./components/ProgressBar"; 4 | 5 | import { TusClientProvider, useTusStore } from "../index"; 6 | import { BasicButton } from "./components/BasicButton"; 7 | import { LoadingCircle } from "./components/LoadingCircle"; 8 | import { UploadIcon } from "./components/UploadIcon"; 9 | import { TUS_DEMO_ENDPOINT } from "./constants"; 10 | 11 | export default { 12 | title: "useTusStore hooks", 13 | } satisfies Meta; 14 | 15 | export const CacheKey = () => ( 16 | 17 | 18 | 19 | ); 20 | 21 | const Uploader = () => { 22 | const inputRef = useRef(null); 23 | const [cacheKey, setCacheKey] = useState("example"); 24 | const { upload, setUpload, isSuccess, isAborted } = useTusStore(cacheKey, { 25 | autoAbort: true, 26 | autoStart: true, 27 | }); 28 | const [progress, setProgress] = useState(0); 29 | const uploadedUrl = useMemo( 30 | () => isSuccess && upload?.url, 31 | [upload, isSuccess] 32 | ); 33 | 34 | const handleOnSelectFile = () => { 35 | if (!inputRef.current) { 36 | return; 37 | } 38 | 39 | inputRef.current.click(); 40 | }; 41 | 42 | const handleOnChangeText = useCallback( 43 | (event: ChangeEvent) => { 44 | setCacheKey(event.target.value); 45 | }, 46 | [] 47 | ); 48 | 49 | const handleOnSetUpload = useCallback( 50 | (event: ChangeEvent) => { 51 | const file = event.target?.files?.item(0); 52 | 53 | if (!file) { 54 | return; 55 | } 56 | 57 | setUpload(file, { 58 | endpoint: TUS_DEMO_ENDPOINT, 59 | chunkSize: file.size / 10, 60 | metadata: { 61 | filename: file.name, 62 | filetype: file.type, 63 | }, 64 | onProgress: (bytesSent, bytesTotal) => { 65 | setProgress(Number(((bytesSent / bytesTotal) * 100).toFixed(2))); 66 | }, 67 | }); 68 | }, 69 | [setUpload] 70 | ); 71 | 72 | const handleOnStart = useCallback(() => { 73 | if (!upload) { 74 | return; 75 | } 76 | 77 | upload.start(); 78 | }, [upload]); 79 | 80 | const handleOnAbort = useCallback(async () => { 81 | if (!upload) { 82 | return; 83 | } 84 | 85 | await upload.abort(); 86 | }, [upload]); 87 | 88 | return ( 89 |
90 |
91 |
92 | 93 |
94 |
95 |

96 | In this demo, you can upload to the demo-only server provided by tus 97 | official. 98 |

99 |

Also, please be careful about the images you upload.

100 |
101 |
102 | Cache key 103 | 109 |
110 |
111 | 112 |
113 | 114 |
115 | 120 | 126 | 132 |
133 | {upload && !isAborted && !isSuccess && ( 134 |
135 | 136 |
137 | )} 138 | {uploadedUrl && ( 139 |
140 | upload 141 |
142 | )} 143 |
144 |
145 | ); 146 | }; 147 | -------------------------------------------------------------------------------- /src/__stories__/DefaultOptions.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from "@storybook/react"; 2 | import { ChangeEvent, useCallback, useMemo, useRef, useState } from "react"; 3 | import { ProgressBar } from "./components/ProgressBar"; 4 | 5 | import { TusClientProvider, DefaultOptions, useTusStore } from "../index"; 6 | import { BasicButton } from "./components/BasicButton"; 7 | import { UploadIcon } from "./components/UploadIcon"; 8 | import { LoadingCircle } from "./components/LoadingCircle"; 9 | import { TUS_DEMO_ENDPOINT } from "./constants"; 10 | 11 | export default { 12 | title: "useTusStore hooks", 13 | } satisfies Meta; 14 | 15 | const defaultOptions: DefaultOptions = (contents) => { 16 | const file = contents instanceof File ? contents : undefined; 17 | 18 | return { 19 | endpoint: TUS_DEMO_ENDPOINT, 20 | chunkSize: file?.size ? file.size / 10 : undefined, 21 | metadata: file 22 | ? { 23 | filename: file.name, 24 | filetype: file.type, 25 | } 26 | : undefined, 27 | }; 28 | }; 29 | 30 | export const WithDefaultOptions = () => ( 31 | 32 | 33 | 34 | ); 35 | 36 | const Uploader = () => { 37 | const inputRef = useRef(null); 38 | const { upload, setUpload, isSuccess, isAborted } = useTusStore("cacheKey", { 39 | autoStart: true, 40 | }); 41 | const [progress, setProgress] = useState(0); 42 | const uploadedUrl = useMemo( 43 | () => isSuccess && upload?.url, 44 | [upload, isSuccess] 45 | ); 46 | 47 | const handleOnSelectFile = () => { 48 | if (!inputRef.current) { 49 | return; 50 | } 51 | 52 | inputRef.current.click(); 53 | }; 54 | 55 | const handleOnSetUpload = useCallback( 56 | (event: ChangeEvent) => { 57 | const file = event.target?.files?.item(0); 58 | 59 | if (!file) { 60 | return; 61 | } 62 | 63 | setUpload(file, { 64 | onProgress: (bytesSent, bytesTotal) => { 65 | setProgress(Number(((bytesSent / bytesTotal) * 100).toFixed(2))); 66 | }, 67 | }); 68 | }, 69 | [setUpload] 70 | ); 71 | 72 | const handleOnStart = useCallback(() => { 73 | if (!upload) { 74 | return; 75 | } 76 | 77 | upload.start(); 78 | }, [upload]); 79 | 80 | const handleOnAbort = useCallback(async () => { 81 | if (!upload) { 82 | return; 83 | } 84 | 85 | await upload.abort(); 86 | }, [upload]); 87 | 88 | return ( 89 |
90 |
91 |
92 | 93 |
94 |
95 |

96 | Here is a demo with defaultOptions specified for TusClientProvider. 97 |

98 |
99 |
100 |

101 | In this demo, you can upload to the demo-only server provided by tus 102 | official. 103 |

104 |

Also, please be careful about the images you upload.

105 |
106 |
107 | 108 |
109 | 110 |
111 | 116 | 122 | 128 |
129 | {upload && !isAborted && !isSuccess && ( 130 |
131 | 132 |
133 | )} 134 | {uploadedUrl && ( 135 |
136 | upload 137 |
138 | )} 139 |
140 |
141 | ); 142 | }; 143 | -------------------------------------------------------------------------------- /src/__stories__/components/BasicButton/BasicButton.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { ComponentProps, FC, forwardRef } from "react"; 3 | 4 | type BasicButtonProps = { 5 | onClick: ComponentProps<"button">["onClick"]; 6 | title: string; 7 | disabled?: ComponentProps<"button">["disabled"]; 8 | styleColor?: "primary" | "basic" | "error"; 9 | }; 10 | 11 | export const BasicButton: FC = forwardRef< 12 | HTMLButtonElement, 13 | BasicButtonProps 14 | >(({ onClick, title, disabled, styleColor = "primary" }, ref) => ( 15 | 34 | )); 35 | -------------------------------------------------------------------------------- /src/__stories__/components/BasicButton/index.ts: -------------------------------------------------------------------------------- 1 | export { BasicButton } from "./BasicButton"; 2 | -------------------------------------------------------------------------------- /src/__stories__/components/LoadingCircle/LoadingCircle.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | 3 | export const LoadingCircle: FC = () => ( 4 |
5 | 6 | 7 | 8 | 9 |
10 | ); 11 | -------------------------------------------------------------------------------- /src/__stories__/components/LoadingCircle/index.ts: -------------------------------------------------------------------------------- 1 | export { LoadingCircle } from "./LoadingCircle"; 2 | -------------------------------------------------------------------------------- /src/__stories__/components/ProgressBar/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | type ProgressBarProps = { 4 | title?: string; 5 | value?: number; 6 | }; 7 | 8 | export const ProgressBar: FC = ({ value = 0, title }) => ( 9 |
10 | {title && ( 11 |
12 | {title} 13 |
14 | )} 15 |
16 |
20 |
21 |
22 | ); 23 | -------------------------------------------------------------------------------- /src/__stories__/components/ProgressBar/index.ts: -------------------------------------------------------------------------------- 1 | export { ProgressBar } from "./ProgressBar"; 2 | -------------------------------------------------------------------------------- /src/__stories__/components/UploadIcon/UploadIcon.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | export const UploadIcon: FC = () => ( 4 |
5 | 12 | 13 | 14 | 15 |
16 | ); 17 | -------------------------------------------------------------------------------- /src/__stories__/components/UploadIcon/index.ts: -------------------------------------------------------------------------------- 1 | export { UploadIcon } from "./UploadIcon"; 2 | -------------------------------------------------------------------------------- /src/__stories__/constants.ts: -------------------------------------------------------------------------------- 1 | export const TUS_DEMO_ENDPOINT = "https://tusd.tusdemo.net/files/" as const; 2 | -------------------------------------------------------------------------------- /src/__stories__/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .sb-show-main.sb-main-padded { 6 | padding: 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/__stories__/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: { 6 | transitionTimingFunction: { 7 | loading: "cubic-bezier(0, 1, 1, 0)", 8 | }, 9 | keyframes: { 10 | "loader-dots1": { 11 | "0%": { 12 | transform: "scale(0)", 13 | }, 14 | "100%": { 15 | transform: "scale(1)", 16 | }, 17 | }, 18 | "loader-dots2": { 19 | "0%": { 20 | transform: "translate(0, 0)", 21 | }, 22 | "100%": { 23 | transform: "translate(24px, 0)", 24 | }, 25 | }, 26 | "loader-dots3": { 27 | "0%": { 28 | transform: "scale(1)", 29 | }, 30 | "100%": { 31 | transform: "scale(0)", 32 | }, 33 | }, 34 | }, 35 | animation: { 36 | loader1: "loader-dots1 0.6s infinite", 37 | loader2: "loader-dots2 0.6s infinite", 38 | loader3: "loader-dots3 0.6s infinite", 39 | }, 40 | }, 41 | }, 42 | variants: { 43 | extend: { 44 | backgroundColor: ["disabled"], 45 | textColor: ["disabled"], 46 | cursor: ["disabled"], 47 | }, 48 | }, 49 | plugins: [], 50 | }; 51 | -------------------------------------------------------------------------------- /src/__tests__/TusClientProvider.test.tsx: -------------------------------------------------------------------------------- 1 | import * as tus from "tus-js-client"; 2 | import { render, act, renderHook, cleanup } from "@testing-library/react"; 3 | import { DefaultOptions, TusClientProvider } from "../TusClientProvider"; 4 | import { createConsoleErrorMock } from "./utils/mock"; 5 | import { getBlob } from "./utils/getBlob"; 6 | import { ERROR_MESSAGES } from "../TusClientProvider/constants"; 7 | import { useTusClientState } from "../TusClientProvider/store/contexts"; 8 | 9 | describe("TusClientProvider", () => { 10 | beforeEach(() => { 11 | // HACK: mock for isSupported property 12 | Object.defineProperty(tus, "isSupported", { 13 | get: jest.fn(() => true), 14 | set: jest.fn(), 15 | }); 16 | 17 | cleanup(); 18 | }); 19 | 20 | it("Should output error message if the browser is not supoprted", () => { 21 | jest.spyOn(tus, "isSupported", "get").mockReturnValue(false); 22 | 23 | const consoleErrorMock = createConsoleErrorMock(); 24 | 25 | act(() => { 26 | render(); 27 | }); 28 | 29 | expect(consoleErrorMock).toHaveBeenCalledWith( 30 | ERROR_MESSAGES.tusIsNotSupported 31 | ); 32 | }); 33 | 34 | describe("Should pass each props", () => { 35 | it("Nothing to pass", async () => { 36 | const { result } = renderHook(() => useTusClientState(), { 37 | wrapper: ({ children }) => ( 38 | {children} 39 | ), 40 | }); 41 | 42 | expect(result.current.defaultOptions?.(getBlob(""))).toBe(undefined); 43 | }); 44 | 45 | it("defaultOptions", async () => { 46 | const defaultOptions: DefaultOptions = (file) => ({ 47 | endpoint: "hoge", 48 | metadata: 49 | file instanceof File 50 | ? { 51 | filename: file.name, 52 | filetype: file.type, 53 | } 54 | : undefined, 55 | }); 56 | 57 | const { result } = renderHook(() => useTusClientState(), { 58 | wrapper: ({ children }) => ( 59 | 60 | {children} 61 | 62 | ), 63 | }); 64 | 65 | expect( 66 | result.current.defaultOptions?.(new File([], "name", { type: "type" })) 67 | ).toStrictEqual({ 68 | endpoint: "hoge", 69 | metadata: { 70 | filename: "name", 71 | filetype: "type", 72 | }, 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/__tests__/createUpload.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { 3 | DetailedError, 4 | HttpRequest, 5 | HttpResponse, 6 | Upload, 7 | } from "tus-js-client"; 8 | import { getBlob } from "./utils/getBlob"; 9 | import { createUpload } from "../utils"; 10 | import { TusHooksUploadFnOptions } from "../types"; 11 | import { createMock } from "./utils/createMock"; 12 | 13 | const blog = getBlob("test"); 14 | 15 | describe("createUpload", () => { 16 | const detailedError = createMock(new Error()); 17 | const onChange = jest.fn(); 18 | const onStart = jest.fn(); 19 | const onAbort = jest.fn(); 20 | const uploadFnOptions: TusHooksUploadFnOptions = { 21 | onAfterResponse: jest.fn(), 22 | onBeforeRequest: jest.fn(), 23 | onChunkComplete: jest.fn(), 24 | onError: jest.fn(), 25 | onProgress: jest.fn(), 26 | onShouldRetry: jest.fn(), 27 | onSuccess: jest.fn(), 28 | }; 29 | 30 | beforeEach(() => { 31 | jest.clearAllMocks(); 32 | }); 33 | 34 | test("Should invoke onChange when some properies changed", async () => { 35 | const { upload } = createUpload({ 36 | Upload, 37 | file: blog, 38 | uploadOptions: {}, 39 | uploadFnOptions, 40 | onChange, 41 | onStart, 42 | onAbort, 43 | }); 44 | 45 | expect(onChange).not.toBeCalled(); 46 | expect(upload.url).toBe(null); 47 | 48 | upload.url = "test"; 49 | expect(onChange).toBeCalledWith(upload); 50 | expect(upload.url).toBe("test"); 51 | 52 | upload.url = null; 53 | expect(onChange).toBeCalledWith(upload); 54 | expect(upload.url).toBe(null); 55 | }); 56 | 57 | test("Should invoke function with upload instance", async () => { 58 | const uploadOptions = {}; 59 | const { upload } = createUpload({ 60 | Upload, 61 | file: blog, 62 | uploadOptions, 63 | uploadFnOptions, 64 | onChange, 65 | onStart, 66 | onAbort, 67 | }); 68 | 69 | expect(uploadFnOptions.onSuccess).not.toBeCalled(); 70 | expect(uploadFnOptions.onError).not.toBeCalled(); 71 | expect(uploadFnOptions.onProgress).not.toBeCalled(); 72 | expect(uploadFnOptions.onShouldRetry).not.toBeCalled(); 73 | expect(uploadFnOptions.onAfterResponse).not.toBeCalled(); 74 | expect(uploadFnOptions.onBeforeRequest).not.toBeCalled(); 75 | expect(uploadFnOptions.onChunkComplete).not.toBeCalled(); 76 | 77 | upload.options?.onSuccess?.(); 78 | expect(uploadFnOptions.onSuccess).toBeCalledWith(upload); 79 | 80 | upload.options?.onError?.(detailedError); 81 | expect(uploadFnOptions.onError).toBeCalledWith(detailedError, upload); 82 | 83 | upload.options?.onProgress?.(1, 2); 84 | expect(uploadFnOptions.onProgress).toBeCalledWith(1, 2, upload); 85 | 86 | upload.options?.onShouldRetry?.(detailedError, 1, uploadOptions); 87 | expect(uploadFnOptions.onShouldRetry).toBeCalledWith( 88 | detailedError, 89 | 1, 90 | uploadOptions, 91 | upload 92 | ); 93 | 94 | const req = createMock(); 95 | const res = createMock(); 96 | upload.options?.onAfterResponse?.(req, res); 97 | expect(uploadFnOptions.onAfterResponse).toBeCalledWith(req, res, upload); 98 | 99 | upload.options?.onBeforeRequest?.(req); 100 | expect(uploadFnOptions.onBeforeRequest).toBeCalledWith(req, upload); 101 | 102 | upload.options?.onChunkComplete?.(1, 2, 3); 103 | expect(uploadFnOptions.onChunkComplete).toBeCalledWith(1, 2, 3, upload); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /src/__tests__/useTus.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook, act, waitFor } from "@testing-library/react"; 2 | import { useRef } from "react"; 3 | import { TusHooksOptions, useTus } from "../index"; 4 | import { getBlob } from "./utils/getBlob"; 5 | import { 6 | createConsoleErrorMock, 7 | createUploadMock, 8 | startOrResumeUploadMock, 9 | } from "./utils/mock"; 10 | import { getDefaultOptions } from "./utils/getDefaultOptions"; 11 | import { UploadFile } from "../types"; 12 | 13 | /* eslint-disable no-console */ 14 | 15 | const originProcess = process; 16 | 17 | const start = jest.fn(); 18 | const abort = jest.fn(); 19 | const Upload = createUploadMock(start, abort); 20 | 21 | const renderUseTus = (initialProps: TusHooksOptions = {}) => 22 | renderHook((props) => useTus(props), { 23 | initialProps: { 24 | ...initialProps, 25 | Upload, 26 | }, 27 | }); 28 | 29 | describe("useTus", () => { 30 | beforeEach(() => { 31 | window.process = originProcess; 32 | jest.resetAllMocks(); 33 | }); 34 | 35 | describe("uploading", () => { 36 | it("Should generate tus instance", async () => { 37 | const { result } = renderUseTus(); 38 | 39 | const file: UploadFile = getBlob("hello"); 40 | const options = getDefaultOptions(); 41 | 42 | expect(result.current).toEqual({ 43 | upload: undefined, 44 | error: undefined, 45 | isSuccess: false, 46 | isAborted: false, 47 | isUploading: false, 48 | setUpload: expect.any(Function), 49 | remove: expect.any(Function), 50 | }); 51 | 52 | act(() => { 53 | result.current.setUpload(file, options); 54 | }); 55 | await waitFor(() => result.current.upload); 56 | 57 | expect(result.current).toEqual({ 58 | upload: expect.any(Upload), 59 | error: undefined, 60 | isSuccess: false, 61 | isAborted: false, 62 | isUploading: false, 63 | setUpload: expect.any(Function), 64 | remove: expect.any(Function), 65 | }); 66 | 67 | act(() => { 68 | result.current.remove(); 69 | }); 70 | await waitFor(() => result.current.upload); 71 | 72 | expect(result.current).toEqual({ 73 | upload: undefined, 74 | isSuccess: false, 75 | error: undefined, 76 | isAborted: false, 77 | isUploading: false, 78 | setUpload: expect.any(Function), 79 | remove: expect.any(Function), 80 | }); 81 | }); 82 | 83 | it("Should setUpload without option args", async () => { 84 | const { result } = renderUseTus({ 85 | uploadOptions: { 86 | endpoint: "hoge", 87 | chunkSize: 100, 88 | }, 89 | }); 90 | 91 | act(() => { 92 | result.current.setUpload(getBlob("hello"), { 93 | endpoint: "hogehoge", 94 | uploadSize: 1000, 95 | }); 96 | }); 97 | 98 | await waitFor(() => result.current.upload); 99 | 100 | expect(result.current.upload?.options.endpoint).toBe("hogehoge"); 101 | expect(result.current.upload?.options.chunkSize).toBe(100); 102 | expect(result.current.upload?.options.uploadSize).toBe(1000); 103 | }); 104 | 105 | it("Should change isSuccess state on success", async () => { 106 | const { result } = renderUseTus({ autoStart: false }); 107 | 108 | expect(result.current).toEqual({ 109 | upload: undefined, 110 | isSuccess: false, 111 | error: undefined, 112 | isAborted: false, 113 | isUploading: false, 114 | setUpload: expect.any(Function), 115 | remove: expect.any(Function), 116 | }); 117 | 118 | const consoleErrorMock = createConsoleErrorMock(); 119 | act(() => { 120 | result.current.setUpload(getBlob("hello"), { 121 | ...getDefaultOptions(), 122 | onSuccess: () => { 123 | console.error(); 124 | }, 125 | }); 126 | }); 127 | 128 | await waitFor(() => result.current.upload); 129 | 130 | expect(result.current).toEqual({ 131 | upload: expect.any(Upload), 132 | isSuccess: false, 133 | error: undefined, 134 | isAborted: false, 135 | isUploading: false, 136 | setUpload: expect.any(Function), 137 | remove: expect.any(Function), 138 | }); 139 | 140 | act(() => { 141 | result.current.upload?.start(); 142 | }); 143 | await waitFor(() => result.current.isUploading); 144 | expect(result.current).toEqual({ 145 | upload: expect.any(Upload), 146 | isSuccess: false, 147 | error: undefined, 148 | isAborted: false, 149 | isUploading: true, 150 | setUpload: expect.any(Function), 151 | remove: expect.any(Function), 152 | }); 153 | 154 | const onSuccess = result.current.upload?.options?.onSuccess; 155 | if (!onSuccess) { 156 | throw new Error("onSuccess is falsly."); 157 | } 158 | 159 | act(() => { 160 | onSuccess(); 161 | }); 162 | 163 | expect(result.current).toEqual({ 164 | upload: expect.any(Upload), 165 | isSuccess: true, 166 | error: undefined, 167 | isAborted: false, 168 | isUploading: false, 169 | setUpload: expect.any(Function), 170 | remove: expect.any(Function), 171 | }); 172 | expect(consoleErrorMock).toHaveBeenCalledWith(); 173 | }); 174 | }); 175 | 176 | it("Should change error state on error", async () => { 177 | const { result } = renderUseTus(); 178 | 179 | expect(result.current).toEqual({ 180 | upload: undefined, 181 | isSuccess: false, 182 | error: undefined, 183 | isAborted: false, 184 | isUploading: false, 185 | setUpload: expect.any(Function), 186 | remove: expect.any(Function), 187 | }); 188 | 189 | const consoleErrorMock = createConsoleErrorMock(); 190 | 191 | act(() => { 192 | result.current.setUpload(getBlob("hello"), { 193 | ...getDefaultOptions(), 194 | onError: () => { 195 | console.error(); 196 | }, 197 | }); 198 | }); 199 | 200 | await waitFor(() => result.current.upload); 201 | 202 | expect(result.current).toEqual({ 203 | upload: expect.any(Upload), 204 | isSuccess: false, 205 | error: undefined, 206 | isAborted: false, 207 | isUploading: false, 208 | setUpload: expect.any(Function), 209 | remove: expect.any(Function), 210 | }); 211 | 212 | const onError = result.current.upload?.options?.onError; 213 | const error = new Error(); 214 | 215 | if (!onError) { 216 | throw new Error("onError is falsly."); 217 | } 218 | 219 | act(() => { 220 | onError(error); 221 | }); 222 | 223 | expect(result.current).toEqual({ 224 | upload: expect.any(Upload), 225 | isSuccess: false, 226 | error, 227 | isAborted: false, 228 | isUploading: false, 229 | setUpload: expect.any(Function), 230 | remove: expect.any(Function), 231 | }); 232 | expect(consoleErrorMock).toHaveBeenCalledWith(); 233 | }); 234 | 235 | it("Should pass `shouldTerminate`", () => { 236 | const { result } = renderUseTus(); 237 | act(() => { 238 | result.current.setUpload(getBlob("hello"), getDefaultOptions()); 239 | }); 240 | 241 | result.current.upload?.abort(true); 242 | expect(abort).toHaveBeenCalledWith(true); 243 | 244 | result.current.upload?.abort(false); 245 | expect(abort).toHaveBeenCalledWith(false); 246 | }); 247 | 248 | describe("Options", () => { 249 | describe("autoAbort", () => { 250 | it("Should abort on unmount", async () => { 251 | const { result, unmount } = renderUseTus({ 252 | autoAbort: true, 253 | }); 254 | 255 | const file: UploadFile = getBlob("hello"); 256 | const options = getDefaultOptions(); 257 | 258 | expect(result.current.upload?.abort).toBeUndefined(); 259 | 260 | act(() => { 261 | result.current.setUpload(file, options); 262 | }); 263 | 264 | expect(abort).not.toHaveBeenCalled(); 265 | 266 | act(() => { 267 | unmount(); 268 | }); 269 | 270 | expect(abort).toHaveBeenCalled(); 271 | }); 272 | 273 | it("Should not abort on unmount", async () => { 274 | const { result, unmount } = renderUseTus({ autoAbort: false }); 275 | 276 | const file: UploadFile = getBlob("hello"); 277 | const options = getDefaultOptions(); 278 | 279 | expect(result.current.upload?.abort).toBeUndefined(); 280 | 281 | act(() => { 282 | result.current.setUpload(file, options); 283 | }); 284 | 285 | expect(abort).not.toHaveBeenCalled(); 286 | 287 | unmount(); 288 | 289 | expect(abort).not.toHaveBeenCalled(); 290 | }); 291 | }); 292 | 293 | describe("autoStart", () => { 294 | it("Should not call startOrResumeUpload function when autoStart is false", async () => { 295 | const { result } = renderUseTus({ 296 | autoAbort: true, 297 | autoStart: false, 298 | }); 299 | 300 | const file: UploadFile = getBlob("hello"); 301 | const options = getDefaultOptions(); 302 | 303 | expect(result.current.upload?.abort).toBeUndefined(); 304 | 305 | act(() => { 306 | result.current.setUpload(file, options); 307 | }); 308 | await waitFor(() => result.current.upload); 309 | 310 | expect(startOrResumeUploadMock).toBeCalledTimes(0); 311 | }); 312 | 313 | it("Should call startOrResumeUpload function when autoStart is true", async () => { 314 | const { result } = renderUseTus({ 315 | autoAbort: true, 316 | autoStart: true, 317 | }); 318 | 319 | const file: UploadFile = getBlob("hello"); 320 | const options = getDefaultOptions(); 321 | 322 | expect(result.current.upload?.abort).toBeUndefined(); 323 | 324 | act(() => { 325 | result.current.setUpload(file, options); 326 | }); 327 | await waitFor(() => result.current.upload); 328 | 329 | expect(startOrResumeUploadMock).toBeCalledTimes(1); 330 | }); 331 | }); 332 | 333 | describe("Rerender", () => { 334 | it("Should rerender when url changed", async () => { 335 | const { result } = renderHook( 336 | (props) => { 337 | const tus = useTus(props); 338 | const renderCountRef = useRef(0); 339 | renderCountRef.current += 1; 340 | return { tus, renderCountRef }; 341 | }, 342 | { initialProps: { Upload } } 343 | ); 344 | 345 | const file: UploadFile = getBlob("hello"); 346 | const options = getDefaultOptions(); 347 | expect(result.current.renderCountRef.current).toBe(1); 348 | 349 | act(() => { 350 | result.current.tus.setUpload(file, options); 351 | }); 352 | await waitFor(() => result.current.tus.upload); 353 | expect(result.current.tus.upload?.url).toBe(null); 354 | expect(result.current.renderCountRef.current).toBe(2); 355 | 356 | act(() => { 357 | if (!result.current.tus.upload) return; 358 | result.current.tus.upload.url = "test"; 359 | }); 360 | await waitFor(() => result.current.tus.upload); 361 | expect(result.current.tus.upload?.url).toBe("test"); 362 | expect(result.current.renderCountRef.current).toBe(3); 363 | }); 364 | }); 365 | }); 366 | }); 367 | -------------------------------------------------------------------------------- /src/__tests__/useTusStore.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook, act, waitFor } from "@testing-library/react"; 2 | import { 3 | TusClientProvider, 4 | TusClientProviderProps, 5 | } from "../TusClientProvider"; 6 | import { getBlob } from "./utils/getBlob"; 7 | import { getDefaultOptions } from "./utils/getDefaultOptions"; 8 | import { DefaultOptions, TusHooksOptions, useTusStore } from ".."; 9 | import { 10 | useTusClientState, 11 | useTusClientDispatch, 12 | } from "../TusClientProvider/store/contexts"; 13 | import { 14 | insertEnvValue, 15 | createConsoleErrorMock, 16 | startOrResumeUploadMock, 17 | createUploadMock, 18 | } from "./utils/mock"; 19 | import { ERROR_MESSAGES } from "../TusClientProvider/constants"; 20 | import { UploadFile } from "../types"; 21 | 22 | /* eslint-disable no-console */ 23 | 24 | const originProcess = process; 25 | const start = jest.fn(); 26 | const abort = jest.fn(); 27 | const Upload = createUploadMock(start, abort); 28 | 29 | const actualTus = 30 | jest.requireActual("tus-js-client"); 31 | 32 | type InitialProps = { 33 | cacheKey?: string; 34 | options?: TusHooksOptions; 35 | }; 36 | const renderUseTusStore = ( 37 | // eslint-disable-next-line default-param-last 38 | initialProps: InitialProps = {}, 39 | providerProps?: TusClientProviderProps 40 | ) => { 41 | const result = renderHook( 42 | ({ cacheKey, options }) => { 43 | const tus = useTusStore(cacheKey ?? "test", options); 44 | const tusClientState = useTusClientState(); 45 | 46 | return { tus, tusClientState }; 47 | }, 48 | { 49 | initialProps, 50 | wrapper: ({ children }) => ( 51 | // eslint-disable-next-line react/jsx-props-no-spreading 52 | {children} 53 | ), 54 | } 55 | ); 56 | 57 | return result; 58 | }; 59 | 60 | describe("useTusStore", () => { 61 | beforeEach(() => { 62 | window.process = originProcess; 63 | jest.resetAllMocks(); 64 | }); 65 | 66 | describe("Uploading", () => { 67 | it("Should generate tus instance", async () => { 68 | const { result, rerender } = renderUseTusStore({ 69 | cacheKey: "test1", 70 | options: { Upload }, 71 | }); 72 | 73 | const file: UploadFile = getBlob("hello"); 74 | const options = getDefaultOptions(); 75 | 76 | expect(result.current.tus).toEqual({ 77 | upload: undefined, 78 | isSuccess: false, 79 | error: undefined, 80 | isAborted: false, 81 | isUploading: false, 82 | setUpload: expect.any(Function), 83 | remove: expect.any(Function), 84 | }); 85 | 86 | act(() => { 87 | result.current.tus.setUpload(file, options); 88 | }); 89 | 90 | expect(result.current.tus).toEqual({ 91 | upload: expect.any(Upload), 92 | isSuccess: false, 93 | error: undefined, 94 | isAborted: false, 95 | isUploading: false, 96 | setUpload: expect.any(Function), 97 | remove: expect.any(Function), 98 | }); 99 | 100 | rerender({ cacheKey: "test2" }); 101 | expect(result.current.tus).toEqual({ 102 | upload: undefined, 103 | isSuccess: false, 104 | error: undefined, 105 | isAborted: false, 106 | isUploading: false, 107 | setUpload: expect.any(Function), 108 | remove: expect.any(Function), 109 | }); 110 | 111 | rerender({ cacheKey: "test1" }); 112 | expect(result.current.tus).toEqual({ 113 | upload: expect.any(Upload), 114 | isSuccess: false, 115 | error: undefined, 116 | isAborted: true, 117 | isUploading: false, 118 | setUpload: expect.any(Function), 119 | remove: expect.any(Function), 120 | }); 121 | 122 | act(() => { 123 | result.current.tus.remove(); 124 | }); 125 | expect(result.current.tus).toEqual({ 126 | upload: undefined, 127 | isSuccess: false, 128 | error: undefined, 129 | isAborted: false, 130 | isUploading: false, 131 | setUpload: expect.any(Function), 132 | remove: expect.any(Function), 133 | }); 134 | }); 135 | }); 136 | 137 | it("Should be reflected onto the TusClientProvider", async () => { 138 | const { result } = renderUseTusStore({ 139 | cacheKey: "test", 140 | options: { Upload }, 141 | }); 142 | 143 | const file: UploadFile = getBlob("hello"); 144 | const options = { 145 | ...getDefaultOptions(), 146 | }; 147 | 148 | expect(result.current.tus.upload).toBeUndefined(); 149 | expect(result.current.tusClientState.defaultOptions).toBeUndefined(); 150 | expect(result.current.tusClientState.uploads).toEqual({}); 151 | 152 | act(() => { 153 | result.current.tus.setUpload(file, options); 154 | }); 155 | 156 | await waitFor(() => result.current.tus.upload); 157 | 158 | const currentTus = result.current.tus; 159 | expect(currentTus.upload).toBeInstanceOf(Upload); 160 | expect(result.current.tusClientState.defaultOptions).toBeUndefined(); 161 | expect(result.current.tusClientState.uploads.test).toBeInstanceOf(Object); 162 | }); 163 | 164 | it("Should setUpload without option args", async () => { 165 | const defaultOptions: DefaultOptions = () => ({ 166 | endpoint: "hoge", 167 | chunkSize: 100, 168 | Upload, 169 | }); 170 | 171 | const { result } = renderUseTusStore(undefined, { 172 | defaultOptions, 173 | }); 174 | 175 | act(() => { 176 | result.current.tus.setUpload(getBlob("hello"), { 177 | endpoint: "hogehoge", 178 | uploadSize: 1000, 179 | }); 180 | }); 181 | 182 | await waitFor(() => result.current.tus.upload?.options); 183 | 184 | expect(result.current.tus.upload?.options.endpoint).toBe("hogehoge"); 185 | expect(result.current.tus.upload?.options.chunkSize).toBe(100); 186 | expect(result.current.tus.upload?.options.uploadSize).toBe(1000); 187 | }); 188 | 189 | describe("Should throw if the TusClientProvider has not found on development env", () => { 190 | beforeEach(() => { 191 | createConsoleErrorMock(); 192 | insertEnvValue({ NODE_ENV: "development" }); 193 | }); 194 | 195 | it("useTus", async () => { 196 | const targetFn = () => renderHook(() => useTusStore("")); 197 | expect(targetFn).toThrow(Error(ERROR_MESSAGES.tusClientHasNotFounded)); 198 | }); 199 | 200 | it("useTusClientState", async () => { 201 | const targetFn = () => renderHook(() => useTusClientState()); 202 | expect(targetFn).toThrow(Error(ERROR_MESSAGES.tusClientHasNotFounded)); 203 | }); 204 | 205 | it("useTusClientDispatch", async () => { 206 | const targetFn = () => renderHook(() => useTusClientDispatch()); 207 | expect(targetFn).toThrow(Error(ERROR_MESSAGES.tusClientHasNotFounded)); 208 | }); 209 | }); 210 | 211 | describe("Should not throw even if the TusClientProvider has not found on production env", () => { 212 | beforeEach(() => { 213 | insertEnvValue({ NODE_ENV: "production" }); 214 | }); 215 | 216 | it("useTus", async () => { 217 | const targetFn = () => renderHook(() => useTusStore("")); 218 | expect(targetFn).toThrow(TypeError); 219 | }); 220 | 221 | it("useTusClientState", async () => { 222 | const targetFn = () => renderHook(() => useTusClientState()); 223 | expect(targetFn).not.toThrow(); 224 | }); 225 | 226 | it("useTusClientDispatch", async () => { 227 | const targetFn = () => renderHook(() => useTusClientDispatch()); 228 | expect(targetFn).not.toThrow(); 229 | }); 230 | }); 231 | 232 | it("Should set tus config from context value", async () => { 233 | const defaultOptions: DefaultOptions = () => ({ 234 | endpoint: "hoge", 235 | }); 236 | 237 | const { result } = renderUseTusStore(undefined, { 238 | defaultOptions, 239 | }); 240 | 241 | expect( 242 | result.current.tusClientState.defaultOptions?.(getBlob("hello")).endpoint 243 | ).toBe("hoge"); 244 | 245 | const file: UploadFile = getBlob("hello"); 246 | 247 | act(() => { 248 | result.current.tus.setUpload(file, {}); 249 | }); 250 | 251 | await waitFor(() => result.current.tus.upload); 252 | 253 | expect(result.current.tus.upload).toBeInstanceOf(actualTus.Upload); 254 | expect(result.current.tus.upload?.options.endpoint).toBe("hoge"); 255 | }); 256 | 257 | it("Should change isSuccess state on success", async () => { 258 | const { result } = renderUseTusStore({ options: { Upload } }); 259 | 260 | expect(result.current.tus).toEqual({ 261 | upload: undefined, 262 | isSuccess: false, 263 | error: undefined, 264 | isAborted: false, 265 | isUploading: false, 266 | setUpload: expect.any(Function), 267 | remove: expect.any(Function), 268 | }); 269 | 270 | const consoleErrorMock = createConsoleErrorMock(); 271 | act(() => { 272 | result.current.tus.setUpload(getBlob("hello"), { 273 | ...getDefaultOptions(), 274 | onSuccess: () => { 275 | console.error(); 276 | }, 277 | }); 278 | }); 279 | 280 | await waitFor(() => result.current.tus.upload); 281 | 282 | expect(result.current.tus).toEqual({ 283 | upload: expect.any(Upload), 284 | isSuccess: false, 285 | error: undefined, 286 | isAborted: false, 287 | isUploading: false, 288 | setUpload: expect.any(Function), 289 | remove: expect.any(Function), 290 | }); 291 | 292 | const onSuccess = result.current.tus.upload?.options?.onSuccess; 293 | if (!onSuccess) { 294 | throw new Error("onSuccess is falsly."); 295 | } 296 | 297 | act(() => { 298 | onSuccess(); 299 | }); 300 | 301 | expect(result.current.tus).toEqual({ 302 | upload: expect.any(Upload), 303 | isSuccess: true, 304 | error: undefined, 305 | isAborted: false, 306 | isUploading: false, 307 | setUpload: expect.any(Function), 308 | remove: expect.any(Function), 309 | }); 310 | expect(consoleErrorMock).toHaveBeenCalledWith(); 311 | }); 312 | 313 | it("Should change error state on error", async () => { 314 | const { result } = renderUseTusStore({ options: { Upload } }); 315 | expect(result.current.tus).toEqual({ 316 | upload: undefined, 317 | isSuccess: false, 318 | error: undefined, 319 | isAborted: false, 320 | isUploading: false, 321 | setUpload: expect.any(Function), 322 | remove: expect.any(Function), 323 | }); 324 | 325 | const consoleErrorMock = createConsoleErrorMock(); 326 | act(() => { 327 | result.current.tus.setUpload(getBlob("hello"), { 328 | ...getDefaultOptions(), 329 | onError: () => { 330 | console.error(); 331 | }, 332 | }); 333 | }); 334 | 335 | await waitFor(() => result.current.tus.upload); 336 | 337 | expect(result.current.tus).toEqual({ 338 | upload: expect.any(Upload), 339 | isSuccess: false, 340 | error: undefined, 341 | isAborted: false, 342 | isUploading: false, 343 | setUpload: expect.any(Function), 344 | remove: expect.any(Function), 345 | }); 346 | 347 | const onError = result.current.tus.upload?.options?.onError; 348 | if (!onError) { 349 | throw new Error("onError is falsly."); 350 | } 351 | 352 | act(() => { 353 | onError(new Error()); 354 | }); 355 | 356 | expect(result.current.tus).toEqual({ 357 | upload: expect.any(Upload), 358 | isSuccess: false, 359 | error: new Error(), 360 | isAborted: false, 361 | isUploading: false, 362 | setUpload: expect.any(Function), 363 | remove: expect.any(Function), 364 | }); 365 | expect(consoleErrorMock).toHaveBeenCalledWith(); 366 | }); 367 | 368 | it("Should pass `shouldTerminate`", () => { 369 | const { result } = renderUseTusStore({ options: { Upload } }); 370 | 371 | act(() => { 372 | result.current.tus.setUpload(getBlob("hello"), getDefaultOptions()); 373 | }); 374 | 375 | result.current.tus.upload?.abort(true); 376 | expect(abort).toHaveBeenCalledWith(true); 377 | 378 | result.current.tus.upload?.abort(false); 379 | expect(abort).toHaveBeenCalledWith(false); 380 | }); 381 | 382 | describe("Options", () => { 383 | describe("autoAbort", () => { 384 | it("Should abort on unmount", async () => { 385 | const { result, rerender, unmount } = renderUseTusStore({ 386 | cacheKey: "test1", 387 | options: { autoAbort: true, Upload }, 388 | }); 389 | 390 | const file: UploadFile = getBlob("hello"); 391 | const options = getDefaultOptions(); 392 | 393 | expect(result.current.tus.upload?.abort).toBeUndefined(); 394 | 395 | act(() => { 396 | result.current.tus.setUpload(file, options); 397 | }); 398 | await waitFor(() => result.current.tus.upload); 399 | 400 | expect(abort).not.toHaveBeenCalled(); 401 | 402 | rerender({ cacheKey: "test2", options: { autoAbort: true } }); 403 | expect(abort).toBeCalledTimes(1); 404 | 405 | rerender({ cacheKey: "test1", options: { autoAbort: true } }); 406 | expect(abort).toBeCalledTimes(1); 407 | 408 | unmount(); 409 | expect(abort).toBeCalledTimes(2); 410 | }); 411 | 412 | it("Should not abort on unmount", async () => { 413 | const { result, rerender, unmount } = renderUseTusStore({ 414 | cacheKey: "test1", 415 | options: { autoAbort: false, Upload }, 416 | }); 417 | 418 | const file: UploadFile = getBlob("hello"); 419 | const options = getDefaultOptions(); 420 | 421 | expect(result.current.tus.upload?.abort).toBeUndefined(); 422 | 423 | act(() => { 424 | result.current.tus.setUpload(file, options); 425 | }); 426 | await waitFor(() => result.current.tus.upload); 427 | expect(abort).not.toHaveBeenCalled(); 428 | 429 | rerender({ cacheKey: "test2", options: { autoAbort: false } }); 430 | expect(abort).not.toHaveBeenCalled(); 431 | 432 | rerender({ cacheKey: "test1", options: { autoAbort: false } }); 433 | expect(abort).not.toHaveBeenCalled(); 434 | 435 | unmount(); 436 | expect(abort).not.toHaveBeenCalled(); 437 | }); 438 | }); 439 | 440 | describe("autoStart", () => { 441 | it("Should not call startOrResumeUpload function when autoStart is false", async () => { 442 | const { result } = renderUseTusStore({ 443 | options: { autoAbort: true, autoStart: false, Upload }, 444 | }); 445 | 446 | const file: UploadFile = getBlob("hello"); 447 | const options = getDefaultOptions(); 448 | 449 | expect(result.current.tus.upload?.abort).toBeUndefined(); 450 | 451 | act(() => { 452 | result.current.tus.setUpload(file, options); 453 | }); 454 | await waitFor(() => result.current.tus.upload); 455 | 456 | expect(startOrResumeUploadMock).toBeCalledTimes(0); 457 | }); 458 | 459 | it("Should call startOrResumeUpload function when autoStart is true", async () => { 460 | const { result } = renderUseTusStore({ 461 | options: { autoAbort: true, autoStart: true, Upload }, 462 | }); 463 | 464 | const file: UploadFile = getBlob("hello"); 465 | const options = getDefaultOptions(); 466 | 467 | act(() => { 468 | expect(result.current.tus.upload?.abort).toBeUndefined(); 469 | }); 470 | 471 | result.current.tus.setUpload(file, options); 472 | await waitFor(() => result.current.tus.upload); 473 | 474 | expect(startOrResumeUploadMock).toBeCalledTimes(1); 475 | }); 476 | }); 477 | }); 478 | }); 479 | -------------------------------------------------------------------------------- /src/__tests__/utils/createMock.ts: -------------------------------------------------------------------------------- 1 | export function createMock(value: unknown = undefined) { 2 | return value as T; 3 | } 4 | -------------------------------------------------------------------------------- /src/__tests__/utils/getBlob.ts: -------------------------------------------------------------------------------- 1 | export const getBlob = (str: string) => Buffer.from(str) as unknown as Blob; 2 | -------------------------------------------------------------------------------- /src/__tests__/utils/getDefaultOptions.ts: -------------------------------------------------------------------------------- 1 | import { TusHooksUploadOptions } from "../../types"; 2 | 3 | export const getDefaultOptions: () => TusHooksUploadOptions = () => ({ 4 | endpoint: "", 5 | uploadUrl: "", 6 | metadata: { 7 | filetype: "text/plain", 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/__tests__/utils/mock.ts: -------------------------------------------------------------------------------- 1 | import type { Upload, UploadOptions } from "tus-js-client"; 2 | import * as startOrResumeUploadObject from "../../utils/core/startOrResumeUpload"; 3 | import { UploadFile } from "../../types"; 4 | import { createMock } from "./createMock"; 5 | 6 | export const createUploadMock = (start: jest.Mock, abort: jest.Mock) => { 7 | class UploadMock { 8 | file: UploadFile; 9 | 10 | options: UploadOptions; 11 | 12 | url: string | null; 13 | 14 | constructor(file: UploadFile, options: UploadOptions) { 15 | this.file = file; 16 | this.options = options; 17 | this.url = null; 18 | } 19 | 20 | start = start; 21 | 22 | abort = abort; 23 | } 24 | 25 | return createMock(UploadMock); 26 | }; 27 | 28 | export const createConsoleErrorMock = () => { 29 | const consoleMock = jest.spyOn(console, "error"); 30 | consoleMock.mockImplementation(() => undefined); 31 | 32 | return consoleMock; 33 | }; 34 | 35 | export const insertEnvValue = (value: NodeJS.Process["env"]) => { 36 | window.process = { 37 | ...window.process, 38 | env: { 39 | ...window.process.env, 40 | ...value, 41 | }, 42 | }; 43 | }; 44 | 45 | export const startOrResumeUploadMock = jest 46 | .spyOn(startOrResumeUploadObject, "startOrResumeUpload") 47 | .mockImplementationOnce(() => jest.fn()); 48 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useTus"; 2 | export * from "./useTusStore"; 3 | export * from "./useTusClient"; 4 | export * from "./TusClientProvider"; 5 | 6 | export type { 7 | TusHooksResult, 8 | TusHooksOptions, 9 | TusHooksUploadFnOptions, 10 | TusHooksUploadOptions, 11 | } from "./types"; 12 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { Upload, UploadOptions } from "tus-js-client"; 3 | 4 | export interface TusHooksOptions { 5 | autoAbort?: boolean; 6 | autoStart?: boolean; 7 | uploadOptions?: TusHooksUploadOptions; 8 | Upload?: typeof Upload; 9 | } 10 | 11 | export type Merge< 12 | T extends Record, 13 | R extends Record 14 | > = { [P in keyof Omit]: T[P] } & R; 15 | 16 | type AddUploadParamater = F extends (...args: infer A) => infer R 17 | ? (...args: [...A, ...[upload: Upload]]) => R 18 | : never; 19 | 20 | type Callbacks = { 21 | [P in keyof T as P extends `on${string}` 22 | ? // eslint-disable-next-line @typescript-eslint/ban-types 23 | NonNullable extends Function 24 | ? P 25 | : never 26 | : never]: T[P]; 27 | }; 28 | 29 | export type TusHooksUploadFnOptions = { 30 | [K in keyof Callbacks]: AddUploadParamater< 31 | Callbacks[K] 32 | >; 33 | }; 34 | 35 | export type TusHooksUploadOptions = Merge< 36 | UploadOptions, 37 | TusHooksUploadFnOptions 38 | >; 39 | 40 | export type UploadFile = Upload["file"]; 41 | export type SetUpload = ( 42 | file: Upload["file"], 43 | options?: TusHooksUploadOptions 44 | ) => void; 45 | 46 | export type TusHooksResultFn = { 47 | setUpload: SetUpload; 48 | remove: () => void; 49 | }; 50 | 51 | export type TusFalselyContext = { 52 | upload: undefined; 53 | error: undefined; 54 | isSuccess: false; 55 | isAborted: false; 56 | isUploading: false; 57 | }; 58 | 59 | export type TusTruthlyContext = { 60 | upload: Upload; 61 | error?: Error; 62 | isSuccess: boolean; 63 | isAborted: boolean; 64 | isUploading: boolean; 65 | }; 66 | 67 | export type TusContext = TusFalselyContext | TusTruthlyContext; 68 | export type TusHooksResult = TusContext & TusHooksResultFn; 69 | -------------------------------------------------------------------------------- /src/useTus/index.ts: -------------------------------------------------------------------------------- 1 | export { useTus } from "./useTus"; 2 | -------------------------------------------------------------------------------- /src/useTus/useTus.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import { type Upload as UploadType } from "tus-js-client"; 3 | import { 4 | TusHooksOptions, 5 | TusHooksResult, 6 | TusTruthlyContext, 7 | TusContext, 8 | TusHooksUploadOptions, 9 | } from "../types"; 10 | import { 11 | mergeUseTusOptions, 12 | createUpload, 13 | startOrResumeUpload, 14 | useAutoAbort, 15 | } from "../utils"; 16 | import { splitTusHooksUploadOptions } from "../utils/core/splitTusHooksUploadOptions"; 17 | 18 | type UseTusInternalState = { 19 | originalAbort: (() => Promise) | undefined; 20 | }; 21 | 22 | const initialTusContext = Object.freeze({ 23 | upload: undefined, 24 | isSuccess: false, 25 | isAborted: false, 26 | isUploading: false, 27 | error: undefined, 28 | } as const satisfies TusContext); 29 | 30 | export const useTus = (baseOption: TusHooksOptions = {}): TusHooksResult => { 31 | const { 32 | autoAbort, 33 | autoStart, 34 | uploadOptions: baseUploadOptions, 35 | Upload, 36 | } = mergeUseTusOptions(baseOption); 37 | 38 | const [tusContext, setTusContext] = useState(initialTusContext); 39 | const [tusInternalState, setTusInternalState] = useState( 40 | { 41 | originalAbort: undefined, 42 | } 43 | ); 44 | 45 | const updateTusTruthlyContext = ( 46 | context: Omit, "upload"> 47 | ) => { 48 | setTusContext((prev) => { 49 | if (prev.upload === undefined) { 50 | return prev; // TODO: Add appriopriate error handling 51 | } 52 | return { ...prev, ...context }; 53 | }); 54 | }; 55 | 56 | const setUpload: TusHooksResult["setUpload"] = useCallback( 57 | (file, options = {}) => { 58 | const targetOptions = { 59 | ...baseUploadOptions, 60 | ...options, 61 | }; 62 | 63 | function onSuccess() { 64 | updateTusTruthlyContext({ isSuccess: true, isUploading: false }); 65 | 66 | targetOptions?.onSuccess?.(upload); 67 | } 68 | 69 | const onError = (error: Error) => { 70 | updateTusTruthlyContext({ 71 | error, 72 | isUploading: false, 73 | }); 74 | targetOptions?.onError?.(error, upload); 75 | }; 76 | 77 | const mergedUploadOptions: TusHooksUploadOptions = { 78 | ...targetOptions, 79 | onSuccess, 80 | onError, 81 | }; 82 | 83 | const onChange = (newUpload: UploadType) => { 84 | // For re-rendering when `upload` object is changed. 85 | setTusContext((prev) => ({ ...prev, upload: newUpload })); 86 | }; 87 | 88 | const onStart = () => { 89 | updateTusTruthlyContext({ isUploading: true, isAborted: false }); 90 | }; 91 | 92 | const onAbort = () => { 93 | updateTusTruthlyContext({ isUploading: false, isAborted: true }); 94 | }; 95 | 96 | const { uploadOptions, uploadFnOptions } = 97 | splitTusHooksUploadOptions(mergedUploadOptions); 98 | 99 | const { upload, originalAbort } = createUpload({ 100 | Upload, 101 | file, 102 | uploadOptions, 103 | uploadFnOptions, 104 | onChange, 105 | onStart, 106 | onAbort, 107 | }); 108 | 109 | if (autoStart) { 110 | startOrResumeUpload(upload); 111 | } 112 | 113 | setTusContext({ 114 | upload, 115 | error: undefined, 116 | isSuccess: false, 117 | isAborted: false, 118 | isUploading: false, 119 | }); 120 | setTusInternalState({ originalAbort }); 121 | }, 122 | [Upload, autoStart, baseUploadOptions] 123 | ); 124 | 125 | const remove = useCallback(() => { 126 | // `upload.abort` function will set `isAborted` state. 127 | // So call the original function for restore state. 128 | tusInternalState?.originalAbort?.(); 129 | setTusContext(initialTusContext); 130 | setTusInternalState({ originalAbort: undefined }); 131 | }, [tusInternalState]); 132 | 133 | const tusResult: TusHooksResult = { 134 | ...tusContext, 135 | setUpload, 136 | remove, 137 | }; 138 | 139 | useAutoAbort({ 140 | upload: tusResult.upload, 141 | abort: tusInternalState.originalAbort, 142 | autoAbort: autoAbort ?? false, 143 | }); 144 | 145 | return tusResult; 146 | }; 147 | -------------------------------------------------------------------------------- /src/useTusClient/index.ts: -------------------------------------------------------------------------------- 1 | export { useTusClient } from "./useTusClient"; 2 | -------------------------------------------------------------------------------- /src/useTusClient/useTusClient.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { 3 | useTusClientState, 4 | useTusClientDispatch, 5 | } from "../TusClientProvider/store/contexts"; 6 | import { 7 | removeUploadInstance, 8 | resetClient, 9 | } from "../TusClientProvider/store/tucClientActions"; 10 | 11 | export const useTusClient = () => { 12 | const tusClientState = useTusClientState(); 13 | const tusClientDispatch = useTusClientDispatch(); 14 | 15 | const state = tusClientState.uploads; 16 | const removeUpload = useCallback( 17 | (cacheKey: string) => { 18 | tusClientDispatch(removeUploadInstance(cacheKey)); 19 | }, 20 | [tusClientDispatch] 21 | ); 22 | const reset = useCallback( 23 | () => tusClientDispatch(resetClient()), 24 | [tusClientDispatch] 25 | ); 26 | 27 | return { 28 | state, 29 | removeUpload, 30 | reset, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/useTusStore/index.ts: -------------------------------------------------------------------------------- 1 | export { useTusStore } from "./useTusStore"; 2 | -------------------------------------------------------------------------------- /src/useTusStore/useTusStore.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from "react"; 2 | import { type Upload as UploadType } from "tus-js-client"; 3 | import { 4 | useTusClientState, 5 | useTusClientDispatch, 6 | } from "../TusClientProvider/store/contexts"; 7 | import { 8 | insertUploadInstance, 9 | removeUploadInstance, 10 | updateUploadContext, 11 | } from "../TusClientProvider/store/tucClientActions"; 12 | import { 13 | TusHooksOptions, 14 | TusHooksResult, 15 | TusHooksUploadOptions, 16 | } from "../types"; 17 | import { 18 | mergeUseTusOptions, 19 | createUpload, 20 | startOrResumeUpload, 21 | useAutoAbort, 22 | splitTusHooksUploadOptions, 23 | } from "../utils"; 24 | 25 | export const useTusStore = ( 26 | cacheKey: string, 27 | baseOption: TusHooksOptions = {} 28 | ): TusHooksResult => { 29 | const { 30 | autoAbort, 31 | autoStart, 32 | uploadOptions: defaultUploadOptions, 33 | Upload, 34 | } = mergeUseTusOptions(baseOption); 35 | const { defaultOptions, uploads } = useTusClientState(); 36 | const tusClientDispatch = useTusClientDispatch(); 37 | 38 | const setUpload: TusHooksResult["setUpload"] = useCallback( 39 | (file, options = {}) => { 40 | const targetOptions = { 41 | ...defaultOptions?.(file), 42 | ...defaultUploadOptions, 43 | ...options, 44 | }; 45 | 46 | const onSuccess = () => { 47 | tusClientDispatch( 48 | updateUploadContext(cacheKey, { isSuccess: true, isUploading: false }) 49 | ); 50 | targetOptions?.onSuccess?.(upload); 51 | }; 52 | 53 | const onError = (error: Error) => { 54 | tusClientDispatch( 55 | updateUploadContext(cacheKey, { error, isUploading: false }) 56 | ); 57 | targetOptions?.onError?.(error, upload); 58 | }; 59 | 60 | const mergedUploadOptions: TusHooksUploadOptions = { 61 | ...targetOptions, 62 | onSuccess, 63 | onError, 64 | }; 65 | 66 | const onChange = (newUpload: UploadType) => { 67 | // For re-rendering when `upload` object is changed. 68 | tusClientDispatch(updateUploadContext(cacheKey, { upload: newUpload })); 69 | }; 70 | 71 | const onStart = () => { 72 | tusClientDispatch( 73 | updateUploadContext(cacheKey, { 74 | isAborted: false, 75 | isUploading: true, 76 | }) 77 | ); 78 | }; 79 | 80 | const onAbort = () => { 81 | tusClientDispatch( 82 | updateUploadContext(cacheKey, { 83 | isAborted: true, 84 | isUploading: false, 85 | }) 86 | ); 87 | }; 88 | 89 | const { uploadOptions, uploadFnOptions } = 90 | splitTusHooksUploadOptions(mergedUploadOptions); 91 | 92 | const { upload } = createUpload({ 93 | Upload, 94 | file, 95 | uploadOptions, 96 | uploadFnOptions, 97 | onChange, 98 | onStart, 99 | onAbort, 100 | }); 101 | 102 | if (autoStart) { 103 | startOrResumeUpload(upload); 104 | } 105 | 106 | tusClientDispatch( 107 | insertUploadInstance(cacheKey, { 108 | upload, 109 | error: undefined, 110 | isSuccess: false, 111 | isAborted: false, 112 | isUploading: false, 113 | }) 114 | ); 115 | }, 116 | [ 117 | Upload, 118 | autoStart, 119 | cacheKey, 120 | defaultOptions, 121 | defaultUploadOptions, 122 | tusClientDispatch, 123 | ] 124 | ); 125 | 126 | const targetTusState = useMemo(() => uploads[cacheKey], [cacheKey, uploads]); 127 | 128 | const remove = useCallback(() => { 129 | targetTusState?.upload?.abort(); 130 | 131 | tusClientDispatch(removeUploadInstance(cacheKey)); 132 | }, [targetTusState, tusClientDispatch, cacheKey]); 133 | 134 | const tusResult: TusHooksResult = targetTusState 135 | ? { 136 | ...targetTusState, 137 | setUpload, 138 | remove, 139 | } 140 | : { 141 | upload: undefined, 142 | error: undefined, 143 | isSuccess: false, 144 | isAborted: false, 145 | isUploading: false, 146 | setUpload, 147 | remove, 148 | }; 149 | 150 | useAutoAbort({ 151 | upload: tusResult.upload, 152 | abort: tusResult.upload?.abort, 153 | autoAbort: autoAbort ?? false, 154 | }); 155 | 156 | return tusResult; 157 | }; 158 | -------------------------------------------------------------------------------- /src/utils/core/createUpload.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { UploadOptions, type Upload as UploadType } from "tus-js-client"; 3 | import { TusHooksUploadFnOptions, UploadFile } from "../../types"; 4 | 5 | export type CreateUploadParams = { 6 | Upload: typeof UploadType; 7 | file: UploadFile; 8 | uploadOptions: UploadOptions; 9 | uploadFnOptions: TusHooksUploadFnOptions; 10 | onChange: (upload: UploadType) => void; 11 | onStart: () => void; 12 | onAbort: () => void; 13 | }; 14 | 15 | const bindOnChange = ( 16 | upload: UploadType, 17 | onChange: (upload: UploadType) => void, 18 | key: keyof UploadType 19 | ) => { 20 | let property = upload[key]; 21 | const originalUrlDescriptor = Object.getOwnPropertyDescriptor(upload, key); 22 | Object.defineProperty(upload, key, { 23 | get() { 24 | return originalUrlDescriptor?.get?.() ?? property; 25 | }, 26 | set(value) { 27 | if (originalUrlDescriptor?.set) { 28 | originalUrlDescriptor.set.call(upload, value); 29 | } else { 30 | property = value; 31 | } 32 | 33 | onChange(this); 34 | }, 35 | }); 36 | }; 37 | 38 | export const createUpload = ({ 39 | Upload, 40 | file, 41 | uploadOptions, 42 | uploadFnOptions, 43 | onChange, 44 | onStart, 45 | onAbort, 46 | }: CreateUploadParams) => { 47 | const upload = new Upload(file, uploadOptions); 48 | const originalStart = upload.start.bind(upload); 49 | const originalAbort = upload.abort.bind(upload); 50 | 51 | const start: UploadType["start"] = (...args) => { 52 | originalStart(...args); 53 | onStart(); 54 | }; 55 | 56 | const abort: UploadType["abort"] = async (...args) => { 57 | originalAbort(...args); 58 | onAbort(); 59 | }; 60 | 61 | upload.start = start; 62 | upload.abort = abort; 63 | 64 | bindOnChange(upload, onChange, "url"); 65 | 66 | Object.entries(uploadFnOptions).forEach(([key, value]) => { 67 | if (typeof value !== "function") { 68 | return; 69 | } 70 | 71 | const bindedFn: any = (...args: any[]) => value(...args, upload as any); 72 | upload.options[key as keyof TusHooksUploadFnOptions] = bindedFn; 73 | }); 74 | 75 | return { upload, originalStart, originalAbort }; 76 | }; 77 | -------------------------------------------------------------------------------- /src/utils/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createUpload"; 2 | export * from "./splitTusHooksUploadOptions"; 3 | export * from "./startOrResumeUpload"; 4 | -------------------------------------------------------------------------------- /src/utils/core/splitTusHooksUploadOptions.ts: -------------------------------------------------------------------------------- 1 | import { UploadOptions } from "tus-js-client"; 2 | import { TusHooksUploadFnOptions, TusHooksUploadOptions } from "../../types"; 3 | 4 | export function splitTusHooksUploadOptions(options: TusHooksUploadOptions) { 5 | const uploadOptions = Object.entries(options).reduce( 6 | (acc, [key, value]) => ({ 7 | ...acc, 8 | [key]: typeof value !== "function" ? value : undefined, 9 | }), 10 | {} 11 | ); 12 | 13 | const uploadFnOptions = Object.entries( 14 | options 15 | ).reduce( 16 | (acc, [key, value]) => ({ 17 | ...acc, 18 | [key]: typeof value === "function" ? value : undefined, 19 | }), 20 | {} 21 | ); 22 | 23 | return { 24 | uploadOptions, 25 | uploadFnOptions, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/core/startOrResumeUpload.ts: -------------------------------------------------------------------------------- 1 | import { Upload } from "tus-js-client"; 2 | 3 | export const startOrResumeUpload = (upload: Upload): void => { 4 | upload.findPreviousUploads().then((previousUploads) => { 5 | if (previousUploads.length) { 6 | upload.resumeFromPreviousUpload(previousUploads[0]); 7 | } 8 | 9 | upload.start(); 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./options"; 2 | export * from "./core"; 3 | -------------------------------------------------------------------------------- /src/utils/options/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./mergeUseTusOptions"; 2 | export * from "./useAutoAbort"; 3 | -------------------------------------------------------------------------------- /src/utils/options/mergeUseTusOptions.ts: -------------------------------------------------------------------------------- 1 | import { Upload } from "tus-js-client"; 2 | import { TusHooksOptions } from "../../types"; 3 | 4 | const defaultUseTusOptionsValue = Object.freeze({ 5 | autoAbort: true, 6 | autoStart: false, 7 | uploadOptions: undefined, 8 | Upload, 9 | } as const satisfies TusHooksOptions); 10 | 11 | export const mergeUseTusOptions = (options: TusHooksOptions) => ({ 12 | ...defaultUseTusOptionsValue, 13 | ...options, 14 | }); 15 | -------------------------------------------------------------------------------- /src/utils/options/useAutoAbort.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Upload } from "tus-js-client"; 3 | 4 | type UseAutoAbortParams = { 5 | upload: Upload | undefined; 6 | abort: (() => Promise) | undefined; 7 | autoAbort: boolean; 8 | }; 9 | 10 | export const useAutoAbort = ({ 11 | upload, 12 | abort, 13 | autoAbort, 14 | }: UseAutoAbortParams) => { 15 | useEffect(() => { 16 | const abortUploading = async () => { 17 | if (!upload || !abort) { 18 | return; 19 | } 20 | 21 | await abort(); 22 | }; 23 | 24 | return () => { 25 | if (!autoAbort) { 26 | return; 27 | } 28 | 29 | abortUploading(); 30 | }; 31 | 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | }, [autoAbort, upload]); 34 | }; 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDirs": ["src"], 4 | "outDir": "dist", 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "lib": ["ESNext", "dom"], 8 | "jsx": "react-jsx", 9 | "checkJs": false, 10 | "skipLibCheck": true, 11 | "declaration": true, 12 | "declarationMap": false, 13 | "sourceMap": false, 14 | "strict": true, 15 | "moduleResolution": "node", 16 | "esModuleInterop": true 17 | }, 18 | "include": ["src/**/*"] 19 | } 20 | --------------------------------------------------------------------------------