chore(dependencies): update libraries and added File Uploader component (#292)
- Bumped dependencies: `lucide-react`, `react-hook-form`, `@supabase/supabase-js`, `@tanstack/react-query`, `@sentry/nextjs`, and more. - Added `react-dropzone` to `@kit/ui` for file upload support. - Adjusted `reset-password.html` to streamline style usage and HTML structure. - Added new translation keys for file upload functionality. - Cleaned up import order in `existing-account-hint.tsx`.
This commit is contained in:
committed by
GitHub
parent
180e0e0c5e
commit
c1fda420e6
247
packages/ui/src/hooks/use-supabase-upload.tsx
Normal file
247
packages/ui/src/hooks/use-supabase-upload.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import {
|
||||
type FileError,
|
||||
type FileRejection,
|
||||
useDropzone,
|
||||
} from 'react-dropzone';
|
||||
|
||||
interface FileWithPreview extends File {
|
||||
preview?: string;
|
||||
errors: readonly FileError[];
|
||||
}
|
||||
|
||||
type UseSupabaseUploadOptions = {
|
||||
/**
|
||||
* Name of bucket to upload files to in your Supabase project
|
||||
*/
|
||||
bucketName: string;
|
||||
/**
|
||||
* Folder to upload files to in the specified bucket within your Supabase project.
|
||||
*
|
||||
* Defaults to uploading files to the root of the bucket
|
||||
*
|
||||
* e.g If specified path is `test`, your file will be uploaded as `test/file_name`
|
||||
*/
|
||||
path?: string;
|
||||
/**
|
||||
* Allowed MIME types for each file upload (e.g `image/png`, `text/html`, etc). Wildcards are also supported (e.g `image/*`).
|
||||
*
|
||||
* Defaults to allowing uploading of all MIME types.
|
||||
*/
|
||||
allowedMimeTypes?: string[];
|
||||
/**
|
||||
* Maximum upload size of each file allowed in bytes. (e.g 1000 bytes = 1 KB)
|
||||
*/
|
||||
maxFileSize?: number;
|
||||
/**
|
||||
* Maximum number of files allowed per upload.
|
||||
*/
|
||||
maxFiles?: number;
|
||||
/**
|
||||
* The number of seconds the asset is cached in the browser and in the Supabase CDN.
|
||||
*
|
||||
* This is set in the Cache-Control: max-age=<seconds> header. Defaults to 3600 seconds.
|
||||
*/
|
||||
cacheControl?: number;
|
||||
/**
|
||||
* When set to true, the file is overwritten if it exists.
|
||||
*
|
||||
* When set to false, an error is thrown if the object already exists. Defaults to `false`
|
||||
*/
|
||||
upsert?: boolean;
|
||||
/**
|
||||
* Supabase client to use for the upload.
|
||||
*/
|
||||
client: SupabaseClient;
|
||||
/**
|
||||
* Callback to call when the upload is successful.
|
||||
*/
|
||||
onUploadSuccess?: (files: string[]) => void;
|
||||
};
|
||||
|
||||
export type UseSupabaseUploadReturn = ReturnType<typeof useSupabaseUpload>;
|
||||
|
||||
/**
|
||||
* Hook to upload files to a Supabase bucket.
|
||||
*
|
||||
* @param options - Options for the upload.
|
||||
* @returns The upload state.
|
||||
*/
|
||||
export const useSupabaseUpload = (options: UseSupabaseUploadOptions) => {
|
||||
const {
|
||||
bucketName,
|
||||
path,
|
||||
allowedMimeTypes = [],
|
||||
maxFileSize = Number.POSITIVE_INFINITY,
|
||||
maxFiles = 1,
|
||||
cacheControl = 3600,
|
||||
upsert = false,
|
||||
client,
|
||||
onUploadSuccess,
|
||||
} = options;
|
||||
|
||||
const [files, setFiles] = useState<FileWithPreview[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [errors, setErrors] = useState<{ name: string; message: string }[]>([]);
|
||||
const [successes, setSuccesses] = useState<string[]>([]);
|
||||
|
||||
const isSuccess = useMemo(() => {
|
||||
if (errors.length === 0 && successes.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (errors.length === 0 && successes.length === files.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [errors.length, successes.length, files.length]);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[], fileRejections: FileRejection[]) => {
|
||||
const validFiles = acceptedFiles
|
||||
.filter((file) => !files.find((x) => x.name === file.name))
|
||||
.map((file) => {
|
||||
(file as FileWithPreview).preview = URL.createObjectURL(file);
|
||||
(file as FileWithPreview).errors = [];
|
||||
|
||||
return file as FileWithPreview;
|
||||
});
|
||||
|
||||
const invalidFiles = fileRejections.map(({ file, errors }) => {
|
||||
(file as FileWithPreview).preview = URL.createObjectURL(file);
|
||||
(file as FileWithPreview).errors = errors;
|
||||
|
||||
return file as FileWithPreview;
|
||||
});
|
||||
|
||||
const newFiles = [...files, ...validFiles, ...invalidFiles];
|
||||
|
||||
setFiles(newFiles);
|
||||
},
|
||||
[files, setFiles],
|
||||
);
|
||||
|
||||
const dropzoneProps = useDropzone({
|
||||
onDrop,
|
||||
noClick: true,
|
||||
accept: allowedMimeTypes.reduce(
|
||||
(acc, type) => ({ ...acc, [type]: [] }),
|
||||
{},
|
||||
),
|
||||
maxSize: maxFileSize,
|
||||
maxFiles: maxFiles,
|
||||
multiple: maxFiles !== 1,
|
||||
});
|
||||
|
||||
const onUpload = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
// [Joshen] This is to support handling partial successes
|
||||
// If any files didn't upload for any reason, hitting "Upload" again will only upload the files that had errors
|
||||
const filesWithErrors = errors.map((x) => x.name);
|
||||
|
||||
const filesToUpload =
|
||||
filesWithErrors.length > 0
|
||||
? [
|
||||
...files.filter((f) => filesWithErrors.includes(f.name)),
|
||||
...files.filter((f) => !successes.includes(f.name)),
|
||||
]
|
||||
: files;
|
||||
|
||||
const responses = await Promise.all(
|
||||
filesToUpload.map(async (file) => {
|
||||
const filePath = path ? `${path}/${file.name}` : file.name;
|
||||
|
||||
const { error } = await client.storage
|
||||
.from(bucketName)
|
||||
.upload(filePath, file, {
|
||||
cacheControl: cacheControl.toString(),
|
||||
upsert,
|
||||
});
|
||||
|
||||
const fullFilePath = [bucketName, filePath].join('/');
|
||||
|
||||
if (error) {
|
||||
return { name: file.name, message: error.message, fullFilePath };
|
||||
} else {
|
||||
return { name: file.name, message: undefined, fullFilePath };
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const responseErrors = responses.filter((x) => x.message !== undefined);
|
||||
|
||||
// if there were errors previously, this function tried to upload the files again so we should clear/overwrite the existing errors.
|
||||
setErrors(responseErrors);
|
||||
|
||||
const responseSuccesses = responses.filter((x) => x.message === undefined);
|
||||
|
||||
const newSuccesses = Array.from(
|
||||
new Set([...successes, ...responseSuccesses.map((x) => x.name)]),
|
||||
);
|
||||
|
||||
setSuccesses(newSuccesses);
|
||||
|
||||
if (responseSuccesses.length > 0) {
|
||||
const files = responseSuccesses.map((item) => {
|
||||
return item.fullFilePath;
|
||||
});
|
||||
|
||||
onUploadSuccess?.(files);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, [
|
||||
files,
|
||||
path,
|
||||
bucketName,
|
||||
errors,
|
||||
successes,
|
||||
onUploadSuccess,
|
||||
client,
|
||||
cacheControl,
|
||||
upsert,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (files.length === 0) {
|
||||
setErrors([]);
|
||||
}
|
||||
|
||||
// If the number of files doesn't exceed the maxFiles parameter, remove the error 'Too many files' from each file
|
||||
if (files.length <= maxFiles) {
|
||||
let changed = false;
|
||||
|
||||
const newFiles = files.map((file) => {
|
||||
if (file.errors.some((e) => e.code === 'too-many-files')) {
|
||||
file.errors = file.errors.filter((e) => e.code !== 'too-many-files');
|
||||
changed = true;
|
||||
}
|
||||
return file;
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
setFiles(newFiles);
|
||||
}
|
||||
}
|
||||
}, [files.length, setFiles, maxFiles, files]);
|
||||
|
||||
return {
|
||||
files,
|
||||
setFiles,
|
||||
successes,
|
||||
isSuccess,
|
||||
loading,
|
||||
errors,
|
||||
setErrors,
|
||||
onUpload,
|
||||
maxFileSize: maxFileSize,
|
||||
maxFiles: maxFiles,
|
||||
allowedMimeTypes,
|
||||
...dropzoneProps,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user