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
306
packages/ui/src/makerkit/dropzone.tsx
Normal file
306
packages/ui/src/makerkit/dropzone.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
} from 'react';
|
||||
|
||||
import { CheckCircle, File, Loader2, Upload, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { type UseSupabaseUploadReturn } from '../hooks/use-supabase-upload';
|
||||
import { cn } from '../lib/utils';
|
||||
import { Button } from '../shadcn/button';
|
||||
import { Trans } from './trans';
|
||||
|
||||
export const formatBytes = (
|
||||
bytes: number,
|
||||
decimals = 2,
|
||||
size?: 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB' | 'EB' | 'ZB' | 'YB',
|
||||
) => {
|
||||
const k = 1000;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
if (bytes === 0 || bytes === undefined) {
|
||||
return size !== undefined ? `0 ${size}` : '0 bytes';
|
||||
}
|
||||
|
||||
const i =
|
||||
size !== undefined
|
||||
? sizes.indexOf(size)
|
||||
: Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
type DropzoneContextType = Omit<
|
||||
UseSupabaseUploadReturn,
|
||||
'getRootProps' | 'getInputProps'
|
||||
>;
|
||||
|
||||
const DropzoneContext = createContext<DropzoneContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
type DropzoneProps = UseSupabaseUploadReturn & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Dropzone = ({
|
||||
className,
|
||||
children,
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
...restProps
|
||||
}: PropsWithChildren<DropzoneProps>) => {
|
||||
const isSuccess = restProps.isSuccess;
|
||||
const isActive = restProps.isDragActive;
|
||||
|
||||
const isInvalid =
|
||||
(restProps.isDragActive && restProps.isDragReject) ||
|
||||
(restProps.errors.length > 0 && !restProps.isSuccess) ||
|
||||
restProps.files.some((file) => file.errors.length !== 0);
|
||||
|
||||
return (
|
||||
<DropzoneContext.Provider value={{ ...restProps }}>
|
||||
<div
|
||||
{...getRootProps({
|
||||
className: cn(
|
||||
'bg-card text-foreground rounded-lg border p-6 text-center transition-colors duration-300',
|
||||
className,
|
||||
isSuccess ? 'border-solid' : 'border-dashed',
|
||||
isActive && 'border-primary',
|
||||
isInvalid && 'border-destructive bg-destructive/10',
|
||||
),
|
||||
})}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{children}
|
||||
</div>
|
||||
</DropzoneContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const DropzoneContent = ({ className }: { className?: string }) => {
|
||||
const {
|
||||
files,
|
||||
setFiles,
|
||||
onUpload,
|
||||
loading,
|
||||
successes,
|
||||
errors,
|
||||
maxFileSize,
|
||||
maxFiles,
|
||||
isSuccess,
|
||||
} = useDropzoneContext();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const exceedMaxFiles = files.length > maxFiles;
|
||||
|
||||
const handleRemoveFile = useCallback(
|
||||
(fileName: string) => {
|
||||
setFiles(files.filter((file) => file.name !== fileName));
|
||||
},
|
||||
[files, setFiles],
|
||||
);
|
||||
|
||||
if (isSuccess) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-row items-center justify-center gap-x-2',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<CheckCircle size={16} className="text-primary" />
|
||||
|
||||
<p className="text-primary text-sm">
|
||||
<Trans
|
||||
i18nKey="common:dropzone.success"
|
||||
values={{ count: files.length }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col', className)}>
|
||||
{files.map((file, idx) => {
|
||||
const fileError = errors.find((e) => e.name === file.name);
|
||||
const isSuccessfullyUploaded = !!successes.find((e) => e === file.name);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${file.name}-${idx}`}
|
||||
className="flex items-center gap-x-4 border-b py-2 first:mt-4 last:mb-4"
|
||||
>
|
||||
{file.type.startsWith('image/') ? (
|
||||
<div className="bg-muted flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded border">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
decoding={'async'}
|
||||
src={file.preview}
|
||||
alt={file.name}
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-muted flex h-10 w-10 items-center justify-center rounded border">
|
||||
<File size={18} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex shrink grow flex-col items-start truncate">
|
||||
<p title={file.name} className="max-w-full truncate text-sm">
|
||||
{file.name}
|
||||
</p>
|
||||
|
||||
{file.errors.length > 0 ? (
|
||||
<p className="text-destructive text-xs">
|
||||
{file.errors
|
||||
.map((e) =>
|
||||
e.message.startsWith('File is larger than')
|
||||
? t('common:dropzone.errorMessageFileSizeTooLarge', {
|
||||
size: formatBytes(file.size, 2),
|
||||
maxSize: formatBytes(maxFileSize, 2),
|
||||
})
|
||||
: e.message,
|
||||
)
|
||||
.join(', ')}
|
||||
</p>
|
||||
) : loading && !isSuccessfullyUploaded ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<Trans i18nKey="common:dropzone.uploading" />
|
||||
</p>
|
||||
) : fileError ? (
|
||||
<p className="text-destructive text-xs">
|
||||
<Trans
|
||||
i18nKey="common:dropzone.errorMessage"
|
||||
values={{ message: fileError.message }}
|
||||
/>
|
||||
</p>
|
||||
) : isSuccessfullyUploaded ? (
|
||||
<p className="text-primary text-xs">
|
||||
<Trans i18nKey="common:dropzone.success" />
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{formatBytes(file.size, 2)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!loading && !isSuccessfullyUploaded && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="link"
|
||||
className="text-muted-foreground hover:text-foreground shrink-0 justify-self-end"
|
||||
onClick={() => handleRemoveFile(file.name)}
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{exceedMaxFiles && (
|
||||
<p className="text-destructive mt-2 text-left text-sm">
|
||||
<Trans
|
||||
i18nKey="common:dropzone.errorMaxFiles"
|
||||
values={{ count: maxFiles, files: files.length - maxFiles }}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
{files.length > 0 && !exceedMaxFiles && (
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onUpload}
|
||||
disabled={files.some((file) => file.errors.length !== 0) || loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Trans i18nKey="common:dropzone.uploading" />
|
||||
</>
|
||||
) : (
|
||||
<span className="flex items-center">
|
||||
<Upload size={20} className="mr-2 h-4 w-4" />
|
||||
|
||||
<Trans
|
||||
i18nKey="common:dropzone.uploadFiles"
|
||||
values={{
|
||||
count: files.length,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DropzoneEmptyState = ({ className }: { className?: string }) => {
|
||||
const { maxFiles, maxFileSize, inputRef, isSuccess } = useDropzoneContext();
|
||||
|
||||
if (isSuccess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center gap-y-2', className)}>
|
||||
<Upload size={20} className="text-muted-foreground" />
|
||||
|
||||
<p className="text-sm">
|
||||
<Trans
|
||||
i18nKey="common:dropzone.uploadFiles"
|
||||
values={{ count: maxFiles }}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col items-center gap-y-1">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<Trans i18nKey="common:dropzone.dragAndDrop" />{' '}
|
||||
<a
|
||||
onClick={() => inputRef.current?.click()}
|
||||
className="hover:text-foreground cursor-pointer underline transition"
|
||||
>
|
||||
<Trans
|
||||
i18nKey="common:dropzone.select"
|
||||
values={{ count: maxFiles === 1 ? `file` : 'files' }}
|
||||
/>
|
||||
</a>{' '}
|
||||
<Trans i18nKey="common:dropzone.toUpload" />
|
||||
</p>
|
||||
|
||||
{maxFileSize !== Number.POSITIVE_INFINITY && (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<Trans
|
||||
i18nKey="common:dropzone.maxFileSize"
|
||||
values={{ size: formatBytes(maxFileSize, 2) }}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const useDropzoneContext = () => {
|
||||
const context = useContext(DropzoneContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useDropzoneContext must be used within a Dropzone');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export { Dropzone, DropzoneContent, DropzoneEmptyState, useDropzoneContext };
|
||||
28
packages/ui/src/makerkit/file-uploader.tsx
Normal file
28
packages/ui/src/makerkit/file-uploader.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { useSupabaseUpload } from '../hooks/use-supabase-upload';
|
||||
import { cn } from '../lib/utils/cn';
|
||||
import { Dropzone, DropzoneContent, DropzoneEmptyState } from './dropzone';
|
||||
|
||||
export const FileUploader = (props: {
|
||||
className?: string;
|
||||
maxFiles: number;
|
||||
bucketName: string;
|
||||
path?: string;
|
||||
allowedMimeTypes: string[];
|
||||
maxFileSize: number | undefined;
|
||||
cacheControl?: number;
|
||||
client: SupabaseClient;
|
||||
onUploadSuccess?: (files: string[]) => void;
|
||||
}) => {
|
||||
const uploader = useSupabaseUpload(props);
|
||||
|
||||
return (
|
||||
<div className={cn(props.className)}>
|
||||
<Dropzone {...uploader}>
|
||||
<DropzoneEmptyState />
|
||||
<DropzoneContent />
|
||||
</Dropzone>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user