├── .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 |
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 |
11 |
12 |
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 | Upload
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 | Upload
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 | Abort
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 |
30 | {title}
31 |
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 |
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 |
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 |
89 |
90 |
91 |
96 |
102 |
108 |
109 | {isUploading && (
110 |
111 |
112 |
113 | )}
114 | {uploadedUrl && (
115 |
116 |
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 |
127 |
128 |
129 |
134 |
140 |
146 |
147 | {isUploading && (
148 |
149 |
150 |
151 | )}
152 | {uploadedUrl && (
153 |
154 |
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 |
113 |
114 |
115 |
120 |
126 |
132 |
133 | {upload && !isAborted && !isSuccess && (
134 |
135 |
136 |
137 | )}
138 | {uploadedUrl && (
139 |
140 |
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 |
109 |
110 |
111 |
116 |
122 |
128 |
129 | {upload && !isAborted && !isSuccess && (
130 |
131 |
132 |
133 | )}
134 | {uploadedUrl && (
135 |
136 |
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 |
32 | {title}
33 |
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 |
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 |
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 |
--------------------------------------------------------------------------------